<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/atom-style.xsl" type="text/xsl"?><feed version="2.0" xmlns="http://www.w3.org/2005/Atom"><id>https://www.kassner.com.br/atom.xml</id><title>Rafael Kassner</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/" language="en"/><link rel="self" type="application/atom+xml" href="https://www.kassner.com.br/atom.xml"/><subtitle>Recent posts from Rafael Kassner</subtitle><author><uri>https://www.kassner.com.br/</uri><name>Rafael Kassner</name></author><generator>https://gohugo.io</generator><updated>2025-05-08T00:00:00Z</updated><entry><title>Delta Chat: encrypted chat with boring technology</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2025/05/08/delta-chat-encrypted-chat-with-boring-technology/"/><published>2025-05-08T00:00:00Z</published><updated>2025-05-08T00:00:00Z</updated><content type="html">&lt;p>Hi,&lt;/p>
&lt;p>A couple of days ago I&amp;rsquo;ve heard about &lt;a href="https://delta.chat">Delta Chat&lt;/a>, and for once I got thrilled with some piece of technology again. At the surface it&amp;rsquo;s just a chat app to the likes of Telegram, Signal and WhatsApp. The exciting part, however, is that is uses plenty of open standards technology under the hood. It&amp;rsquo;s a chat app built on top the regular e-mail protocols, SMTP and IMAP, with a sprinkle of PGP to make it end-to-end encrypted.&lt;/p>
&lt;p>It has the same &lt;em>secure initial key exchange&lt;/em> issue that PGP e-mail has, but at least now we have an interface that is straight up easy to use, specially for people outside of the infosec world. It support rich media (images, videos, voice messages) and even groups (untested, but I assume it won&amp;rsquo;t allow newjoiners to read old messages).&lt;/p>
&lt;p>&lt;a href="/images/2025-05-delta-chat/iphone-client.jpg">&lt;img alt="Delta Chat on iPhone" loading="lazy" src="/images/2025-05-delta-chat/iphone-client.jpg">&lt;/a>&lt;/p>
&lt;p>What makes me so excited about this is that each message is sent as a plain e-mail message. One MTA sending an e-mail to another MTA. Simple, old, functional. I know e-mail, I have my own server, I can just go to my maildir and read everything. Can I?&lt;/p>
&lt;h3 id="reading-delta-chat-messages-from-the-mail-server">Reading Delta Chat messages from the mail server&lt;/h3>
&lt;p>I&amp;rsquo;ve used the iOS app for my initial tests, and you can absolutely export your entire chat history decrypted in the app, but I want to prove this point to myself. Let&amp;rsquo;s try with a message found in my maildir:&lt;/p>
&lt;pre tabindex="0">&lt;code>Return-Path: &amp;lt;client2@example.com&amp;gt;
Delivered-To: client1@example.com
Content-Type: multipart/encrypted; protocol=&amp;#34;application/pgp-encrypted&amp;#34;;
boundary=&amp;#34;183d0f93c6372f58_1fb940b49edbf5c5_64e1f6b034240ef1&amp;#34;
From: &amp;lt;client2@example.com&amp;gt;
To: &amp;lt;client1@example.com&amp;gt;
Subject: [...]
Date: Tue, 6 May 2025 21:07:17 +0000
Message-ID: &amp;lt;0b5a2c48-472d-45b9-8096-219313f5dea1@localhost&amp;gt;
In-Reply-To: &amp;lt;3325e202-0957-4fd3-ae69-1a8fa94c16ba@localhost&amp;gt;
References: &amp;lt;51784fab-1af1-4ad3-a0b6-a868bd2ad685@localhost&amp;gt;
&amp;lt;51784fab-1af1-4ad3-a0b6-a868bd2ad685@localhost&amp;gt;
&amp;lt;3325e202-0957-4fd3-ae69-1a8fa94c16ba@localhost&amp;gt;
Chat-Version: 1.0
Autocrypt: addr=client2@example.com; prefer-encrypt=mutual; keydata=[...]
--183d0f93c6372f58_1fb940b49edbf5c5_64e1f6b034240ef1
Content-Type: application/octet-stream; name=&amp;#34;encrypted.asc&amp;#34;;
charset=&amp;#34;utf-8&amp;#34;
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename=&amp;#34;encrypted.asc&amp;#34;;
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
hF4D6OmtN[...]s9SkewYsB
-----END PGP MESSAGE-----
--183d0f93c6372f58_1fb940b49edbf5c5_64e1f6b034240ef1--
&lt;/code>&lt;/pre>&lt;p>I recognize these &lt;code>PGP MESSAGE&lt;/code> headers. Where is the private key? In the backup I&amp;rsquo;ve exported before. It can be extracted with this command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>sqlite3 dc_database_backup.sqlite &lt;span style="color:#e6db74">&amp;#39;select hex(private_key) from keypairs limit 1&amp;#39;&lt;/span> | xxd -r -p &amp;gt; deltachat.key
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Decrypting the PGP message with the exported key, sure enough, reveals our content:&lt;/p>
&lt;pre tabindex="0">&lt;code>Content-Type: text/plain; charset=&amp;#34;utf-8&amp;#34;; protected-headers=&amp;#34;v1&amp;#34;
From: &amp;#34;Test 2&amp;#34; &amp;lt;client2@example.com&amp;gt;
To: &amp;#34;Test 1&amp;#34; &amp;lt;client1@example.com&amp;gt;
Subject: Re: Message from Test 1
Date: Tue, 6 May 2025 21:07:17 +0000
In-Reply-To: &amp;lt;3325e202-0957-4fd3-ae69-1a8fa94c16ba@localhost&amp;gt;
References: &amp;lt;51784fab-1af1-4ad3-a0b6-a868bd2ad685@localhost&amp;gt;
&amp;lt;51784fab-1af1-4ad3-a0b6-a868bd2ad685@localhost&amp;gt;
&amp;lt;3325e202-0957-4fd3-ae69-1a8fa94c16ba@localhost&amp;gt;
Chat-Version: 1.0
Chat-Disposition-Notification-To: client2@example.com
Chat-Verified: 1
Message-ID: &amp;lt;0b5a2c48-472d-45b9-8096-219313f5dea1@localhost&amp;gt;
Content-Transfer-Encoding: 7bit
Hello world!
&lt;/code>&lt;/pre>&lt;h3 id="outro">Outro&lt;/h3>
&lt;p>I&amp;rsquo;ve tested Delta Chat with my own mail server, which uses Postfix and has everything configured for public e-mail, like DKIM signing, spamd, IP blocklist checks and so on, and each message took about 2 seconds from one device to another. Using a &lt;a href="https://delta.chat/en/chatmail">public server&lt;/a> it sure feels below 300ms, so there is room for improvement when self-hosting a dedicated &lt;a href="https://github.com/chatmail/relay">chatserver&lt;/a>.&lt;/p>
&lt;p>I really love this, and I apologize in advance to my friends whom will be pestered to move to Delta Chat.&lt;/p>
&lt;p>Thank you!&lt;/p></content><id>https://www.kassner.com.br/en/2025/05/08/delta-chat-encrypted-chat-with-boring-technology/</id><category term="pgp"/><category term="encryption"/><category term="chat"/><category term="email"/></entry><entry><title>Resizing a LUKS-backed BTRFS RAID1 filesystem</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2025/04/12/luks-btrfs-raid1-resize/"/><published>2025-04-12T00:00:00Z</published><updated>2025-04-12T00:00:00Z</updated><content type="html">&lt;p>Hi.&lt;/p>
&lt;p>After a series of hardware upgrades, I finally have 2x 2TB SDD for data on my &lt;a href="/en/2023/05/16/reusing-old-hardware/">NAS&lt;/a>! However, they are still configured with ~900GiB data partitions, and today it is time to expand it!&lt;/p>
&lt;h2 id="initial-state">Initial state&lt;/h2>
&lt;p>The current disk layout is:&lt;/p>
&lt;pre tabindex="0">&lt;code># fdisk -l /dev/sda
Disk /dev/sda: 1.82 TiB, 2000398934016 bytes, 3907029168 sectors
Disk model: PNY CS900 2TB SS
Sector size (logical/physical): 512 bytes / 512 bytes
Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 499711 497664 243M 83 Linux
/dev/sda2 499712 43468799 42969088 20.5G 83 Linux
/dev/sda3 43468800 1918322687 1874853888 894G 83 Linux
# fdisk -l /dev/sdb
Disk /dev/sdb: 1.82 TiB, 2000398934016 bytes, 3907029168 sectors
Disk model: Samsung SSD 870
Sector size (logical/physical): 512 bytes / 512 bytes
Device Start End Sectors Size Type
/dev/sdb1 2048 1001471 999424 488M Linux filesystem
/dev/sdb2 1001472 98656255 97654784 46.6G Linux filesystem
/dev/sdb3 98656256 1973510143 1874853888 894G Linux filesystem
&lt;/code>&lt;/pre>&lt;p>Reading the output above, the data partitions can be found at &lt;code>/dev/sd*3&lt;/code>, both sized at 894GiB with the exact same sector size (&lt;code>512 bytes&lt;/code>) and sector count (&lt;code>1874853888&lt;/code>).&lt;/p>
&lt;p>Both partitions are LUKS encrypted, and I&amp;rsquo;m using crypt keys to avoid typing passwords all the time. They are declared in &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/crypttab.html">crypttab&lt;/a> to be auto-mounted on boot:&lt;/p>
&lt;pre tabindex="0">&lt;code># cat /etc/crypttab | grep data
data1 /dev/sda3 /etc/cryptkey-cs900 luks,discard,timeout=10
data3 /dev/sdb3 /etc/cryptkey-evo870 luks,discard,timeout=10
&lt;/code>&lt;/pre>&lt;p>And finally, both encrypted partitions are used to create a single BTRFS filesystem in RAID1 mode:&lt;/p>
&lt;pre tabindex="0">&lt;code># btrfs filesystem show /data
Label: none uuid: 079c6185-4a18-4f8f-8a62-bb741aabb758
Total devices 2 FS bytes used 661GiB
devid 1 size 894GiB used 661GiB path /dev/mapper/data1
devid 2 size 894GiB used 661GiB path /dev/mapper/data3
&lt;/code>&lt;/pre>&lt;h2 id="the-challenge">The challenge&lt;/h2>
&lt;p>The disks have between 920GiB and 946GiB of free space that I want to reclaim. For consistency, I&amp;rsquo;ll resize them both to reclaim 920GiB, keeping the number of sectors identical.&lt;/p>
&lt;blockquote>
&lt;p>Note: BTRFS&amp;rsquo; RAID1 does not &lt;em>need&lt;/em> identical sizes for each partition, it will keep allocating blocks into its devices as needed, and you could use uneven disk sizes (i.e.: 1x2TB + 2x2TB) in RAID1 mode. As long as it can write each block to two devices, it doesn&amp;rsquo;t matter which ones they are. &lt;a href="https://btrfs.readthedocs.io/en/latest/mkfs.btrfs.html#profile-layout">Read more&lt;/a>.&lt;/p>&lt;/blockquote>
&lt;p>So I am dealing with three layers: disk partitions, LUKS and BTRFS. I&amp;rsquo;ll have to resize them all, starting from the outer one.&lt;/p>
&lt;h3 id="step-0-backup">Step 0: backup&lt;/h3>
&lt;p>Make sure you have &lt;a href="/en/2020/12/19/zfs-backup-strategy/">backups&lt;/a>, and you&amp;rsquo;re fairly confident you can restore your data from them.&lt;/p>
&lt;h3 id="step-1-resize-the-partitions">Step 1: resize the partitions&lt;/h3>
&lt;p>I didn&amp;rsquo;t spend much time investigating if I would be able to do an online resize, I went ahead and booted in the rescue mode I had in GRUB.&lt;/p>
&lt;p>I&amp;rsquo;ve used &lt;a href="https://man7.org/linux/man-pages/man8/cfdisk.8.html">cfdisk&lt;/a>, but any partition manager should be able to do the job. The only important thing (for me), was to keep the same number of sectors for both partitions. I first resized the disk with less free space and rebooted into the system to make sure it was still working. Had I screwed it up, I could recover by &lt;code>dd&lt;/code>-ing the second disk back into the first. Thankfully it wasn&amp;rsquo;t needed.&lt;/p>
&lt;p>Once both partitions were resized, I&amp;rsquo;ve booted back into the regular mode.&lt;/p>
&lt;h3 id="step-2-resize-the-luks-container">Step 2: resize the LUKS container&lt;/h3>
&lt;p>This step was fairly straight-forward, I ran &lt;a href="https://man7.org/linux/man-pages/man8/cryptsetup-resize.8.html">cryptsetup resize&lt;/a> once for each LUKS container. By default, it will expand to use the entire partition.&lt;/p>
&lt;pre tabindex="0">&lt;code># cryptsetup --key-file=/etc/cryptkey-cs900 resize data1
# cryptsetup --key-file=/etc/cryptkey-evo870 resize data3
&lt;/code>&lt;/pre>&lt;h3 id="step-3-resize-the-btrfs-devices">Step 3: resize the BTRFS devices&lt;/h3>
&lt;p>Also fairly simple, I ran &lt;a href="https://btrfs.readthedocs.io/en/latest/btrfs-filesystem.html#man-filesystem-resize">btrfs filesystem resize&lt;/a>. The caveat is that you have to run it for each device in your filesystem (devid column in the &lt;code>btrfs filesystem show&lt;/code> output).&lt;/p>
&lt;pre tabindex="0">&lt;code># btrfs filesystem resize 1:max /data
# btrfs filesystem resize 2:max /data
&lt;/code>&lt;/pre>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>My system can now store 1.77TiB of data, and I&amp;rsquo;ve already put it in use.&lt;/p>
&lt;pre tabindex="0">&lt;code># btrfs filesystem show /data
Label: none uuid: 079c6185-4a18-4f8f-8a62-bb741aabb758
Total devices 2 FS bytes used 1.16TiB
devid 1 size 1.77TiB used 1.17TiB path /dev/mapper/data1
devid 2 size 1.77TiB used 1.17TiB path /dev/mapper/data3
&lt;/code>&lt;/pre>&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2025/04/12/luks-btrfs-raid1-resize/</id><category term="linux"/></entry><entry><title>Home A-CI-stant</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2025/03/23/home-a-ci-stant/"/><published>2025-03-23T00:00:00Z</published><updated>2025-03-23T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>For years I lurked in the Home Assistant channel of a community Discord, but never had much interest in it. Earlier this month I had some discount coupons in hand and decided that it was time to give it a go. I got Home Assistant, the ZBT-1 and one IKEA Inspelning and managed to make it control my balcony lights. In my opinion, the &lt;code>sun&lt;/code> &amp;ldquo;device&amp;rdquo; in Home Assistant might be the most underrated out-of-the-box integration!&lt;/p>
&lt;p>In any case, I live in an apartment, so after that I was mostly done. I don&amp;rsquo;t have much else to automate, and I do really value physical buttons, so it all sat there for a while. This weekend, while doing some maintenance on my NAS, it got me thinking: what if I use Home Assistant to manage a Continuous Integration server?&lt;/p>
&lt;p>I run my own &lt;a href="https://forgejo.org">Forgejo&lt;/a> instance, and I&amp;rsquo;ve been meaning to use the Actions feature for a while, but never got around it in my NAS because I don&amp;rsquo;t want to deal with VMs nor Docker-in-Docker. I also have a fairly decent Optiplex laying around, and while I already had it running a Forgejo Actions runner before, I simply have way to little usage of it to justify have it running 24x7.&lt;/p>
&lt;p>So now it finally clicked. The Optiplex has a feature to ATX-power-on the motherboard once it is energized, and combining that with the smart plug, I can power it on just when there is demand. With that, I made a daemon that will wait for Forgejo&amp;rsquo;s webhooks, and turn on the CI if needed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">m&lt;/span>.&lt;span style="color:#a6e22e">HandleFunc&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;POST /webhook&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">w&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ResponseWriter&lt;/span>, &lt;span style="color:#a6e22e">r&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">payload&lt;/span>.&lt;span style="color:#a6e22e">Repository&lt;/span>.&lt;span style="color:#a6e22e">FullName&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;canastra.online/monorepo&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">WriteHeader&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">StatusNoContent&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">version&lt;/span>, &lt;span style="color:#a6e22e">ok&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">strings&lt;/span>.&lt;span style="color:#a6e22e">CutPrefix&lt;/span>(&lt;span style="color:#a6e22e">payload&lt;/span>.&lt;span style="color:#a6e22e">Ref&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;refs/tags/&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> !&lt;span style="color:#a6e22e">ok&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">WriteHeader&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">StatusNoContent&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">isOn&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">hassIsOn&lt;/span>(&lt;span style="color:#a6e22e">hassEntityId&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ..&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">isOn&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">WriteHeader&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">StatusNoContent&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">hassTurnOn&lt;/span>(&lt;span style="color:#a6e22e">hassEntityId&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Waking up CI server for %s:%s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">payload&lt;/span>.&lt;span style="color:#a6e22e">Repository&lt;/span>.&lt;span style="color:#a6e22e">FullName&lt;/span>, &lt;span style="color:#a6e22e">version&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">WriteHeader&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">StatusNoContent&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>However, that only addresses half of the issue. Once the server is on, it must be powered off in case it&amp;rsquo;s not needed anymore. A systemd timer can take care of this, powering off the server 30 minutes after boot. We can abuse the runner&amp;rsquo;s &lt;a href="https://code.forgejo.org/forgejo/runner/src/tag/v6.3.0/internal/pkg/config/config.example.yaml">shutdown_timeout&lt;/a> to make systemd wait for the running jobs to complete.&lt;/p>
&lt;pre tabindex="0">&lt;code># forgejo-runner.service
[Service]
TimeoutStopSec=30m # same as your runner&amp;#39;s shutdown_timeout
# hass-ci.service
[Service]
ExecStart=systemctl stop forgejo-runner.service &amp;amp;&amp;amp; systemctl poweroff
TimeoutStopSec=30m
# hass-ci.timer
[Timer]
OnBootSec=30m
&lt;/code>&lt;/pre>&lt;p>Check out the complete &lt;a href="https://github.com/kassner/home-assistant-ci-automation">code&lt;/a>, MIT licensed.&lt;/p>
&lt;p>The last piece of the puzzle is a Home Assistant automation that will turn off the plug once the server is completely off.&lt;/p>
&lt;p>&lt;a href="/images/2025-03-home-a-ci-stant/automation.png">&lt;img alt="home assistant automation to turn off the plug when not used" loading="lazy" src="/images/2025-03-home-a-ci-stant/automation.png">&lt;/a>&lt;/p>
&lt;p>And that&amp;rsquo;s about it. Granted, it&amp;rsquo;s totally unnecessary, I could&amp;rsquo;ve just used Wake-Up on LAN, but it was the perfect project for me to dig into Home Assistant and tell myself I need just one more smart plug.&lt;/p>
&lt;p>Thank you!&lt;/p></content><id>https://www.kassner.com.br/en/2025/03/23/home-a-ci-stant/</id><category term="linux"/><category term="ci-cd"/><category term="homeassistant"/><category term="go"/></entry><entry><title>Internet via IPv6</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2024/10/12/internet-via-ipv6/"/><published>2024-10-12T00:00:00Z</published><updated>2024-10-12T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I know I&amp;rsquo;m late to the party, but with the IPv4 exhaustion now being expressed in monetary costs by VPS/Cloud providers, it is time to seek IPv6. My main use-case is to be able to reach servers exclusively over IPv6, so I don&amp;rsquo;t need to pay for IPv4 assignments if those servers will not serve direct public traffic (i.e.: database, storage and job servers).&lt;/p>
&lt;p>My home provider &lt;a href="https://blog.hqcodeshop.fi/archives/376-Com-Hem-offering-IPv6-via-DHCPv6-to-its-customers.html">did have&lt;/a> IPv6 support, but &lt;a href="https://computersweden.se/article/1288414/darfor-satter-com-hem-kundernas-ipv6-pa-paus-techworld.html">they pulled the plug&lt;/a> in 2020, and now 3 years later, I still don&amp;rsquo;t have IPv6 access. Let&amp;rsquo;s fix that today.&lt;/p>
&lt;p>I had a brief experience experience with &lt;a href="https://www.sixxs.net/main/">SixXS&lt;/a> back in 2013, and I&amp;rsquo;ve learned the importance of latency for such tunnels (150ms+ is NOT ok), so my only option is &lt;a href="https://tunnelbroker.net/">tunnelbroker.net&lt;/a>, operated by HE.NET, which has a PoP 10ms away from me.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>I run an &lt;a href="https://eu.store.ui.com/eu/en/products/er-x">EdgeRouter X&lt;/a> as my gateway. The configuration is fairly straight forward:&lt;/p>
&lt;pre tabindex="0">&lt;code>root@edgerouter# show interfaces tunnel tun0
address &amp;lt;Client IPv6 Address&amp;gt;/64
description &amp;#34;HE.NET Tunnel&amp;#34;
encapsulation sit
firewall {
in {
ipv6-name WANv6_IN
}
local {
ipv6-name WANv6_LOCAL
}
}
local-ip &amp;lt;Client IPv4 Address&amp;gt;
multicast disable
remote-ip &amp;lt;Server IPv4 Address&amp;gt;
ttl 255
root@edgerouter# show interfaces switch switch0
address 192.168.1.1/24
address &amp;lt;Routed /64 prefix&amp;gt;::1/64
[...]
root@edgerouter# show protocols static
interface-route6 ::/0 {
next-hop-interface tun0 {
}
}
&lt;/code>&lt;/pre>&lt;p>And the firewall configuration:&lt;/p>
&lt;pre tabindex="0">&lt;code>root@edgerouter# show firewall ipv6-name
ipv6-name WANv6_IN {
default-action drop
description &amp;#34;WAN to LAN&amp;#34;
enable-default-log
rule 10 {
action accept
description &amp;#34;Allow established/related&amp;#34;
state {
established enable
related enable
}
}
rule 20 {
action drop
description &amp;#34;Drop invalid state&amp;#34;
state {
invalid enable
}
}
rule 30 {
action accept
description &amp;#34;Allow IPv6 ICMP&amp;#34;
protocol ipv6-icmp
}
}
ipv6-name WANv6_LOCAL {
default-action drop
description &amp;#34;WAN to Router&amp;#34;
enable-default-log
rule 10 {
action accept
description &amp;#34;Allow established/related&amp;#34;
state {
established enable
related enable
}
}
rule 20 {
action drop
description &amp;#34;Drop invalid state&amp;#34;
state {
invalid enable
}
}
rule 30 {
action accept
description &amp;#34;Allow IPv6 ICMP&amp;#34;
protocol ipv6-icmp
}
}
&lt;/code>&lt;/pre>&lt;p>The values for the items between brackets are to be obtained from the tunnel details page on tunnelbroker.net.&lt;/p>
&lt;p>Notably missing from the configuration is DHCPv6, Prefix Delegation and Route Advertisement. I decided to not allow for auto-configuration in my network, but rather manually configure the machines I want with IPv6. Here&amp;rsquo;s why:&lt;/p>
&lt;h2 id="caveats">Caveats&lt;/h2>
&lt;p>The HE.NET tunnel works. I&amp;rsquo;m able to reach servers via SSH, HTTP and other protocols just fine. Speed isn&amp;rsquo;t great, but given it&amp;rsquo;s a free service, I can&amp;rsquo;t complain.&lt;/p>
&lt;p>&lt;a href="/images/2024-10-internet-via-ipv6/speedtest.png">&lt;img alt="IPv4 and IPv6 speed test" loading="lazy" src="/images/2024-10-internet-via-ipv6/speedtest.png">&lt;/a>&lt;/p>
&lt;blockquote>
&lt;p>A tangential observation: &lt;a href="https://www.fast.com">fast.com&lt;/a> and &lt;a href="https://speedtest.net">speedtest.net&lt;/a> both couldn&amp;rsquo;t test the speed correctly. fast.com used GeoIP to infer my location (sending all the requests from Stockholm to Los Angeles, because HE.NET GeoIP locations). &lt;a href="https://ipv6.speedtest.net">ipv6.speedtest.net&lt;/a> sent requests to dual-stack hostnames, so some of the packets did not go through the tunnel.&lt;/p>&lt;/blockquote>
&lt;p>However, free things are prone to abuse, and the HE.NET tunnel is no different, having fell into despair on the wide internet. This can be translated as &lt;em>CAPTCHAs everywhere&lt;/em> and Google simply refusing to serve any request coming from their IP range:&lt;/p>
&lt;p>&lt;a href="/images/2024-10-internet-via-ipv6/google.png">&lt;img alt="Google blocking HE.NET IPv6 tunnel" loading="lazy" src="/images/2024-10-internet-via-ipv6/google.png">&lt;/a>&lt;/p>
&lt;p>I&amp;rsquo;ve looked into getting a &lt;a href="https://www.ripe.net/manage-ips-and-asns/ipv6/request-ipv6/how-to-request-an-ipv6-pi-assignment/">IPv6 PI Assignment&lt;/a> from RIPE NCC, but HE.NET charges a $500 setup fee for new BGP sessions, not to mention the LIR sponsorship costs, so it quickly becomes too expensive compared to renting IPv4s from VPS/Cloud providers.&lt;/p>
&lt;p>Lastly, HE.NET only supports SIT/GRE tunnels, which don&amp;rsquo;t work behind CGNAT. If you are behind CGNAT, look for WireGuard tunnels as an alternative.&lt;/p>
&lt;h2 id="outro">Outro&lt;/h2>
&lt;p>I&amp;rsquo;m happy with the HE.NET service. It fixes my problem and the limitations are manageable for now. If you are in a similar situation and is looking for a IPv6 tunnel, &lt;a href="https://tunnelbroker.services/">tunnelbroker.services&lt;/a> has a small list of IPv6 tunnel services. Alternatively, if you just need IPv6 to test an HTTP endpoint, a SOCKS proxy with &lt;code>ssh -D 1337 your-ipv6-enabled-vps&lt;/code> is often enough.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2024/10/12/internet-via-ipv6/</id><category term="network"/></entry><entry><title>Auto-deployments with systemd</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/11/05/autodeployment-with-systemd/"/><published>2023-11-05T00:00:00Z</published><updated>2023-11-05T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I like Kubernetes, and having written my own operators in the past, you quickly grow fond of the high level of automation that it allows you to achieve. However, now that I&amp;rsquo;m spending the days building &lt;a href="/projects/">my own projects&lt;/a>, my requirements (and budget) can&amp;rsquo;t fit k8s anymore. A couple of production VPSs and my GitHub Actions free quota is all I&amp;rsquo;m allowing myself to afford on vanity projects, so let&amp;rsquo;s work with what we have.&lt;/p>
&lt;p>GitHub Actions already builds the &lt;a href="/projects/canastra/">Canastra&lt;/a> binary, so I&amp;rsquo;m just missing is a way to auto-deploy it to the production VPS. This is composed of two steps:&lt;/p>
&lt;h2 id="copying-the-binary-to-the-vps">Copying the binary to the VPS&lt;/h2>
&lt;p>This can be as simple as creating a new Linux user, generating a SSH keypair and using scp/rsync. I&amp;rsquo;m going the extra mile to have an user that can&amp;rsquo;t do anything else, which means chroot. My &lt;code>/etc/ssh/sshd_config&lt;/code> now has:&lt;/p>
&lt;pre tabindex="0">&lt;code>Match Group sftpusers
AuthorizedKeysFile /etc/ssh/sftp-authorized-keys/%u
ChrootDirectory /data/sftp/%u
ForceCommand internal-sftp
&lt;/code>&lt;/pre>&lt;p>It worked when &lt;code>scp&lt;/code>ing from my machine, but when inside the GitHub Action, I got a &lt;code>This service allows sftp connections only&lt;/code> error. Maybe something related to TTY? The &lt;code>sftp&lt;/code> command, however, worked, so I used this:&lt;/p>
&lt;pre tabindex="0">&lt;code>- name: push to production
if: github.ref == &amp;#39;refs/heads/main&amp;#39;
run: |
mkdir -p ~/.ssh
echo &amp;#34;$SSH_KNOWN_HOSTS&amp;#34; &amp;gt;&amp;gt; ~/.ssh/known_hosts
eval &amp;#34;$(ssh-agent)&amp;#34;
ssh-add - &amp;lt;&amp;lt;&amp;lt; &amp;#34;$SSH_AUTH_KEY&amp;#34;
cat &amp;lt;&amp;lt;EOF | sftp user@server
cd bin/
put canastra-server
put canastra-server.sha256
bye
EOF
env:
SSH_AUTH_KEY: ${{ secrets.PROD_UPLOAD_SSH_KEY }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
SSH_KNOWN_HOSTS: ${{ secrets.PROD_SSH_KNOWN_HOSTS }}
&lt;/code>&lt;/pre>&lt;h2 id="restarting-the-service">Restarting the service&lt;/h2>
&lt;p>Once the files were uploaded, all I needed was to run a single shell script in the server to restart the server. A common way to achieve this is using SSH&amp;rsquo;s &lt;a href="https://man.openbsd.org/OpenBSD-current/man5/sshd_config.5#ForceCommand">ForceCommand&lt;/a> feature, which unfortunately doesn&amp;rsquo;t work if you are already limiting the user to SFTP-only. You could have a second user that would be able to only run the script, but that&amp;rsquo;s way too much user management for my taste.&lt;/p>
&lt;p>Thankfully, systemd offers a neat solution: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html">systemd.path&lt;/a>. My &lt;code>canastra-deploy.path&lt;/code> unit looks like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>[Unit]
Description=Monitor /data/sftp/canastra/bin for new uploads
[Path]
PathChanged=/data/sftp/canastra/bin/canastra-server.sha256
Unit=canastra-deploy.service
[Install]
WantedBy=multi-user.target
&lt;/code>&lt;/pre>&lt;p>And the service unit:&lt;/p>
&lt;pre tabindex="0">&lt;code>[Unit]
Description=Canastra Deployer
[Service]
ExecStart=/usr/local/bin/canastra-deploy.sh
&lt;/code>&lt;/pre>&lt;p>With this approach, systemd monitors for changes in a particular file, and triggers the configured &lt;code>Unit=&lt;/code> once the target is modified. It&amp;rsquo;s important to use &lt;code>PathChanged=&lt;/code> instead of &lt;code>PathModified=&lt;/code> to avoid triggering the unit too early. From the manual:&lt;/p>
&lt;blockquote>
&lt;p>PathChanged= may be used to watch a file or directory and activate the configured unit whenever it changes. It is not activated on every write to the watched file but it is activated if the file which was open for writing gets closed. PathModified= is similar, but additionally it is activated also on simple writes to the watched file&lt;/p>&lt;/blockquote>
&lt;p>That&amp;rsquo;s all! Once I push a commit to the &lt;code>main&lt;/code> branch, GitHub Actions will build it, and copy the new binary to the server. systemd, once sees a change in the binary file, triggers the deploy script that eventually restarts the service, loading the new version.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/11/05/autodeployment-with-systemd/</id><category term="linux"/><category term="ci-cd"/></entry><entry><title>Launching: canastra.online</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/10/15/canastra-online/"/><published>2023-10-15T00:00:00Z</published><updated>2023-10-15T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>Today I&amp;rsquo;m launching &lt;a href="https://canastra.online/">canastra.online&lt;/a>! It&amp;rsquo;s a website I&amp;rsquo;ve built where you can play Canastra for free, without ads, both single-player and multi-player.&lt;/p>
&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Canasta">Canastra&lt;/a> is a card game, quite popular in Southern Brazil. There are several variations of the game, but for now &lt;a href="https://canastra.online/">canastra.online&lt;/a> only supports one set. &lt;a href="https://canastra.online/en/rules">Read the rules&lt;/a> on our website.&lt;/p>
&lt;h2 id="building">Building&lt;/h2>
&lt;p>I&amp;rsquo;ve built the game in 2018 to play with my significant other, as we couldn&amp;rsquo;t find anything that was both multiplatform and that supported the subset of rules we wanted to play. Since then, our families and us play it on a weekly basis.&lt;/p>
&lt;p>This year, after becoming unemployed, I put my efforts into polishing and finishing the game for public release. Right now I&amp;rsquo;m bearing all the costs alone, but if you want to contribute, check our &lt;a href="https://canastra.online/en/donate">donations page&lt;/a>.&lt;/p>
&lt;h2 id="future">Future&lt;/h2>
&lt;p>There aren&amp;rsquo;t any plans for the future of the game just now. The bot needs more work, as just taking random actions isn&amp;rsquo;t smart. I&amp;rsquo;ll also likely introduce new game modes, or the ability to customize the rules for each game.&lt;/p>
&lt;p>However, one things is clear: the game will always be free and without ads. If there is demand I might implement a few paid resources, but the game as it is today will always be free without ads.&lt;/p>
&lt;p>Thank you!&lt;/p></content><id>https://www.kassner.com.br/en/2023/10/15/canastra-online/</id><category term="project"/><category term="canastra"/></entry><entry><title>Project: Canastra on the go</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/10/06/canastra-on-the-go/"/><published>2023-10-06T00:00:00Z</published><updated>2023-10-06T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I&amp;rsquo;ve spent a considerable amount of time this year rewriting my &lt;a href="/projects/canastra/">Canastra&lt;/a> project in Go. While I wanted to write a specific post about this conversion, it has been over 6 months, so it&amp;rsquo;s unlikely it will still happen. For the purposes of this post though, all it matters is that the project is now built in Go, but the frontend and database architectures are still the same as described in the &lt;a href="/projects/canastra/">project page&lt;/a>, with Go now handling all the HTTP and Websocket communication.&lt;/p>
&lt;p>So a few weeks ago I got a newsletter from my local Raspberry Pi reseller stating the Pi Zero 2 was back in stock and something clicked for me: what if I run Canastra on the Pi, so wife and I can play it on the airplane when we&amp;rsquo;re travelling? The project is already in Go, so cross-compiling for ARM should be trivial. So I bought the Pi Zero, together with the official case, for a bit less than 300 SEK.&lt;/p>
&lt;h2 id="challenges">Challenges&lt;/h2>
&lt;h3 id="booting">Booting&lt;/h3>
&lt;p>In my first attempt I &lt;code>dd&lt;/code>-ed the Raspberry Pi OS Lite to a SD card and booted in the Pi, just to realize that it uses mini-HDMI (as opposed o micro-HDMI from the Raspberry Pi 4B) and I did&amp;rsquo;t want spend any more money in this project. The latest versions of Pi OS will prompt you to create a user during the first boot, so just &lt;code>touch ssh&lt;/code> in the boot partition isn&amp;rsquo;t enough, as there isn&amp;rsquo;t a user to login yet. Thankfully the &lt;a href="https://www.raspberrypi.com/news/raspberry-pi-imager-imaging-utility/">Pi Imager&lt;/a> is able to produce a SSH-able image, although it is quite annoying that you have to use a special software instead of just &lt;code>dd if=pi-os.img &amp;amp;&amp;amp; touch /mnt/boot/ssh&lt;/code>.&lt;/p>
&lt;h3 id="wi-fi-ap">Wi-Fi AP&lt;/h3>
&lt;p>There are several great tutorials for how to turn a Raspberry Pi into a Wireless access point, and the steps are roughly 1) run &lt;code>hostapd&lt;/code>; 2) run a DHCP server; and 3) IPTables for routing. While there is nothing out of the ordinary here, the fact that I only had SSH access to the Pi Zero meant I couldn&amp;rsquo;t get &lt;code>hostapd&lt;/code> running because I was relying on the Wi-Fi connection to SSH into it, and bringing &lt;code>hostapd&lt;/code> up meant this connection would drop. I ended up plugging the SD card into a Pi 4B, &lt;code>chroot&lt;/code> into the SD card, run all the commands, unmount, plug it back into the Pi Zero. Some screw-ups meant that the Pi Zero would boot but not connect to my home network neither create the AP, so I had to do this steps maybe 3 or 4 times until I got it right.&lt;/p>
&lt;p>The DHCP server I ran with dnsmasq, mostly because I also wanted to run a DNS server to be able to configure a &lt;a href="https://en.wikipedia.org/wiki/Captive_portal">Captive Portal&lt;/a>. Given the only use of the Pi Zero will be for the game, I just redirected everything to the Go application using &lt;code>address=/#/192.168.1.1&lt;/code> and added extra checks in the Go side for the redirect:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">CaptivePortalHandler&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">handler&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ServeMux&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">c&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">CaptivePortalHandler&lt;/span>) &lt;span style="color:#a6e22e">ServeHTTP&lt;/span>(&lt;span style="color:#a6e22e">w&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ResponseWriter&lt;/span>, &lt;span style="color:#a6e22e">r&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">r&lt;/span>.&lt;span style="color:#a6e22e">Host&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">os&lt;/span>.&lt;span style="color:#a6e22e">Getenv&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;HOST&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">url&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Sprintf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;https://%s/&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">os&lt;/span>.&lt;span style="color:#a6e22e">Getenv&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;HOST&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">Header&lt;/span>().&lt;span style="color:#a6e22e">Add&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Location&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">url&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">WriteHeader&lt;/span>(&lt;span style="color:#ae81ff">302&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">w&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>([]&lt;span style="color:#66d9ef">byte&lt;/span>{})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">handler&lt;/span>.&lt;span style="color:#a6e22e">ServeHTTP&lt;/span>(&lt;span style="color:#a6e22e">w&lt;/span>, &lt;span style="color:#a6e22e">r&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// on func main()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ListenAndServe&lt;/span>(&lt;span style="color:#a6e22e">listenAddr&lt;/span>, &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">CaptivePortalHandler&lt;/span>{&lt;span style="color:#a6e22e">handler&lt;/span>: &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">handler&lt;/span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="data-storage">Data storage&lt;/h3>
&lt;p>Canastra in production uses a PostgreSQL database to store all the game events, a single row for each event/player action. It&amp;rsquo;s not that the Raspberry Pi wouldn&amp;rsquo;t be able to run Postgres, but I wanted to store the events in a file in disk for this case, mostly so I could experiment supporting multiple persistence layers in Go. For that, I refactored my persistence layer in a way that I could support both Postgres and JSON files, just replacing the &lt;code>DATABASE_URL&lt;/code> environment variable, so the changes between production and the Pi Zero build would be minimal.&lt;/p>
&lt;p>Given this project doesn&amp;rsquo;t use CGO, compiling was a simple matter of using &lt;code>GOARCH=arm64&lt;/code> and uploading the binary to the Pi.&lt;/p>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>I did a &lt;a href="https://phpc.social/@kassner/111186776012752441">test run last night&lt;/a> and it was successful!&lt;/p>
&lt;style>
div.gallery {
margin: 1rem 0 1rem 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
a, img {
margin: 0;
padding: 0;
box-shadow: 0 0;
}
img {
display: block;
}
}
&lt;/style>
&lt;div class="gallery">
&lt;a href="https://www.kassner.com.br/images/2023-10-canastra-on-the-go/pi-zero.jpg">&lt;img src="https://www.kassner.com.br/images/2023-10-canastra-on-the-go/pi-zero.jpg" alt="Raspberry Pi Zero 2 W in its official case connected to a 2200mAh power bank" title="Raspberry Pi Zero 2 W in its official case connected to a 2200mAh power bank">&lt;/a>
&lt;a href="https://www.kassner.com.br/images/2023-10-canastra-on-the-go/wifi.jpg">&lt;img src="https://www.kassner.com.br/images/2023-10-canastra-on-the-go/wifi.jpg" alt="Wi-Fi selection on iPhone showing it connected to the Wi-Fi network provided by the Raspberry Pi" title="Wi-Fi selection on iPhone showing it connected to the Wi-Fi network provided by the Raspberry Pi">&lt;/a>
&lt;a href="https://www.kassner.com.br/images/2023-10-canastra-on-the-go/captive-portal.jpg">&lt;img src="https://www.kassner.com.br/images/2023-10-canastra-on-the-go/captive-portal.jpg" alt="Canastra home page opened in the Captive Portal, running on the Raspberry Pi" title="Canastra home page opened in the Captive Portal, running on the Raspberry Pi">&lt;/a>
&lt;/div>
&lt;p>The game works as intended, I can see no performance differences, even saving the events to the SD card.&lt;/p>
&lt;p>Given the power requirements for the Pi Zero are quite low, I can run it from a powerbank for several hours, or even from the USB port on the in-flight entertainment. I&amp;rsquo;m already looking forward for our next trip in December, I&amp;rsquo;ll report back here then with a quick update.&lt;/p>
&lt;p>Thank you!&lt;/p></content><id>https://www.kassner.com.br/en/2023/10/06/canastra-on-the-go/</id><category term="linux"/><category term="hardware"/><category term="network"/><category term="go"/><category term="project"/><category term="canastra"/></entry><entry><title>New project: What to cook?</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/09/21/what-to-cook-launch/"/><published>2023-09-21T00:00:00Z</published><updated>2023-09-21T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>For a few years now, my partner and I had a dilemma almost daily: what would we make for lunch/dinner that day? It&amp;rsquo;s not a matter of lack of skill or ideas for recipes, or even having the ingredients available, but more of a &lt;em>nothing sounds appealing for the amount of effort I want to put in cooking this meal&lt;/em> type of situation.&lt;/p>
&lt;p>On top of that, I am particularly terrible to remember things I could cook. We&amp;rsquo;d have something that we both truly enjoyed, yet would not make it again for a long time simple because it was forgotten as an option. I need to see what options I have, and then &lt;em>maybe&lt;/em> I can choose.&lt;/p>
&lt;p>So for a few months I was joking about this idea, that I&amp;rsquo;d create some software that would just tell us what to cook. The internet is mostly converging to this approach too, with personalized feeds and &amp;ldquo;for you&amp;rdquo; pages, so we might as well take inspiration on the worse parts of what the internet has to offer.&lt;/p>
&lt;blockquote>
&lt;p>You don&amp;rsquo;t know what to make for dinner? Click this button, and you&amp;rsquo;ll have a recipe. You don&amp;rsquo;t have a particular ingredient at home? No worries, exclude it, and no recipes with that ingredient will be offered again tonight. Don&amp;rsquo;t feel like cooking this recipe? Well, too bad, you won&amp;rsquo;t get another. You can&amp;rsquo;t just refresh the page or start over, you&amp;rsquo;ll only be able to get a different recipe in a few hours.&lt;/p>&lt;/blockquote>
&lt;p>Jokes aside, I don&amp;rsquo;t think this is a problem that only we have, although I believe most people have the opposite problem: too many ideas for a single meal. I tried to search for projects that tried to tackle this problem, but most would just hover around voting systems or recipe management. I want something less democratic, something that takes the burden off of deciding what to eat.&lt;/p>
&lt;p>So I built &lt;a href="https://github.com/kassner/whattocook">What to cook?&lt;/a>. Run with Docker, input your recipes with the ingredients and click the button. The project is open to external contributions, but I plan to keep the project around the scope I presented here. I don&amp;rsquo;t want to turn it into another recipe management software, as there are tons of them out there. Maybe send a PR to import recipes from your favorite database?&lt;/p>
&lt;p>I just started using it with my partner, and we&amp;rsquo;re still loading the database with our recipes as we cook them. My expectation is that it can go both ways: either we rely on it more than we currently want to admit, or it forces us to think better about what we want. In either case, I see it as a win.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/09/21/what-to-cook-launch/</id><category term="java"/><category term="project"/></entry><entry><title>fail2ban + Caddy with JSON logs</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/09/10/fail2ban-caddy-json-logs/"/><published>2023-09-10T00:00:00Z</published><updated>2023-09-10T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I&amp;rsquo;m running &lt;a href="https://www.caddyserver.io/">Caddy&lt;/a> and saving access logs to disk in the JSON format. I want to integrate fail2ban to block bots trying &lt;code>/wp-login.php&lt;/code> and other known URLs, and I couldn&amp;rsquo;t find much about how to make fail2ban read Caddy&amp;rsquo;s logs.&lt;/p>
&lt;p>This is a hack that I quickly came up with, barely tested, but I managed to make it work:&lt;/p>
&lt;p>&lt;code>/etc/fail2ban/filter.d/caddy-forbidden.local&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>[Definition]
failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:403
datepattern = &amp;#34;ts&amp;#34;:&amp;lt;DATE&amp;gt;\.
ignoreregex =
&lt;/code>&lt;/pre>&lt;p>Append to &lt;code>/etc/fail2ban/jail.local&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>[caddy-forbidden]
port = http,https
logpath = /var/log/caddy/*.log
enabled = true
&lt;/code>&lt;/pre>&lt;p>So an entry like this will successfully match and ban the IP &lt;code>192.0.2.6&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>{&amp;#34;level&amp;#34;:&amp;#34;info&amp;#34;,&amp;#34;ts&amp;#34;:1694328885.166141,&amp;#34;logger&amp;#34;:&amp;#34;http.log.access.log2&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;handled request&amp;#34;,&amp;#34;request&amp;#34;:{&amp;#34;remote_ip&amp;#34;:&amp;#34;192.0.2.6&amp;#34;,&amp;#34;remote_port&amp;#34;:&amp;#34;64305&amp;#34;,&amp;#34;client_ip&amp;#34;:&amp;#34;192.0.2.6&amp;#34;,&amp;#34;proto&amp;#34;:&amp;#34;HTTP/1.1&amp;#34;,&amp;#34;method&amp;#34;:&amp;#34;GET&amp;#34;,&amp;#34;host&amp;#34;:&amp;#34;www.kassner.com.br&amp;#34;,&amp;#34;uri&amp;#34;:&amp;#34;/wp-login.php&amp;#34;,&amp;#34;headers&amp;#34;:{...},&amp;#34;tls&amp;#34;:{...}},...,&amp;#34;status&amp;#34;:403,...}
&lt;/code>&lt;/pre>&lt;p>Lastly, I&amp;rsquo;ve added to my &lt;code>Caddyfile&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>(common) {
error /wp-login.php &amp;#34;Forbidden&amp;#34; 403
}
&lt;/code>&lt;/pre>&lt;p>&amp;ndash;&lt;/p>
&lt;p>It&amp;rsquo;s worth noting this isn&amp;rsquo;t well tested, and given there is user input in the logs, you might ran into false-positives with a well-crafted URL or headers. &lt;strong>Use at your own risk&lt;/strong>.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/09/10/fail2ban-caddy-json-logs/</id><category term="caddy"/><category term="http"/><category term="linux"/></entry><entry><title>Using a Java class for DB migration</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/08/29/java-db-migration/"/><published>2023-08-29T00:00:00Z</published><updated>2023-08-29T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I got some good feedback from &lt;a href="https://fosstodon.org/@phxql/110950660023257282">@phxql@fosstodon.org&lt;/a> related to my &lt;a href="/en/2023/08/25/first-spring-project/">previous post&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>with flyway it&amp;rsquo;s possible to have a JavaMigration, which let&amp;rsquo;s you write custom code.&lt;/p>&lt;/blockquote>
&lt;p>So let&amp;rsquo;s test this out today!&lt;/p>
&lt;h2 id="javamigration">JavaMigration&lt;/h2>
&lt;p>It took me a while to make this work. I couldn&amp;rsquo;t figure it out what it was meant by &lt;em>adding the class to the &lt;code>db.migration&lt;/code> package&lt;/em>. Some places mentioned &lt;code>src/db/migration&lt;/code>, some places mentioned &lt;code>src/main/java/db/migration&lt;/code> and I even tried &lt;code>src/main/resources/db/migration&lt;/code> (shouldn&amp;rsquo;t work, but here are my SQL files), but it was fruitless. In the end, &lt;code>@ComponentScan&lt;/code> helped me again, as I created a new sub-package &lt;code>migration&lt;/code> in my Spring project and annotated my class with &lt;code>@Component&lt;/code>, that made it work, with the added benefit of allowing me to control the location of the files.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> com.ytemail.migration;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.flywaydb.core.api.migration.BaseJavaMigration;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.flywaydb.core.api.migration.Context;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.springframework.security.crypto.password.PasswordEncoder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.springframework.stereotype.Component;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@Component&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">V3__RecalculateHashes&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> BaseJavaMigration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">private&lt;/span> PasswordEncoder passwordEncoder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#a6e22e">V3__RecalculateHashes&lt;/span>(PasswordEncoder passwordEncoder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">passwordEncoder&lt;/span> &lt;span style="color:#f92672">=&lt;/span> passwordEncoder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">migrate&lt;/span>(Context context) &lt;span style="color:#66d9ef">throws&lt;/span> Exception
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// code goes here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Given Spring also initializes this object, I can use dependency injection to get any service or Bean I need to handle the migration. I couldn&amp;rsquo;t, however, use a Repository, as Spring would consider it a circular dependency:&lt;/p>
&lt;pre tabindex="0">&lt;code>***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| flyway defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]
↑ ↓
| v3__RecalculateHashes defined in file [/home/kassner/workspace/ytemail/build/classes/java/main/com/ytemail/migration/V3__RecalculateHashes.class]
↑ ↓
| userRepository defined in com.ytemail.repository.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration
↑ ↓
| jpaSharedEM_entityManagerFactory
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
&lt;/code>&lt;/pre>&lt;p>So we must do things the old way today:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">migrate&lt;/span>(Context context) &lt;span style="color:#66d9ef">throws&lt;/span> Exception
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.&lt;span style="color:#a6e22e">getConnection&lt;/span>().&lt;span style="color:#a6e22e">beginRequest&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PreparedStatement updateStmt &lt;span style="color:#f92672">=&lt;/span> context.&lt;span style="color:#a6e22e">getConnection&lt;/span>().&lt;span style="color:#a6e22e">prepareStatement&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;UPDATE public.user SET password = ? WHERE id = ?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PreparedStatement selectStmt &lt;span style="color:#f92672">=&lt;/span> context.&lt;span style="color:#a6e22e">getConnection&lt;/span>().&lt;span style="color:#a6e22e">prepareStatement&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;SELECT id, email FROM public.user&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ResultSet resultSet &lt;span style="color:#f92672">=&lt;/span> selectStmt.&lt;span style="color:#a6e22e">executeQuery&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (resultSet.&lt;span style="color:#a6e22e">next&lt;/span>()) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Long id &lt;span style="color:#f92672">=&lt;/span> resultSet.&lt;span style="color:#a6e22e">getLong&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> String email &lt;span style="color:#f92672">=&lt;/span> resultSet.&lt;span style="color:#a6e22e">getString&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;email&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> String password &lt;span style="color:#f92672">=&lt;/span> resultSet.&lt;span style="color:#a6e22e">getString&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;password&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> updateStmt.&lt;span style="color:#a6e22e">setString&lt;/span>(1, passwordEncoder.&lt;span style="color:#a6e22e">encode&lt;/span>(convertOldHash(password)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> updateStmt.&lt;span style="color:#a6e22e">setLong&lt;/span>(2, id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> updateStmt.&lt;span style="color:#a6e22e">executeUpdate&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.&lt;span style="color:#a6e22e">getConnection&lt;/span>().&lt;span style="color:#a6e22e">commit&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="callback">Callback&lt;/h2>
&lt;p>Flyway has the concept of Callbacks, which allows you to plug either a SQL file or a Java class to run after each &lt;a href="https://github.com/flyway/flyway/blob/flyway-9.21.2/flyway-core/src/main/java/org/flywaydb/core/api/callback/Event.java">Event&lt;/a>. You can also leverage that to run some Java code.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> com.ytemail.migration;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.flywaydb.core.api.callback.Callback;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.flywaydb.core.api.callback.Context;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.flywaydb.core.api.callback.Event;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.springframework.stereotype.Component;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@Component&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Test1&lt;/span> &lt;span style="color:#66d9ef">implements&lt;/span> Callback
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">boolean&lt;/span> &lt;span style="color:#a6e22e">supports&lt;/span>(Event event, Context context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (event &lt;span style="color:#f92672">!=&lt;/span> Event.&lt;span style="color:#a6e22e">AFTER_EACH_MIGRATE&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#f92672">!&lt;/span>context.&lt;span style="color:#a6e22e">getMigrationInfo&lt;/span>().&lt;span style="color:#a6e22e">getVersion&lt;/span>().&lt;span style="color:#a6e22e">getVersion&lt;/span>().&lt;span style="color:#a6e22e">equals&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;3.0.1&amp;#34;&lt;/span>)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">boolean&lt;/span> &lt;span style="color:#a6e22e">canHandleInTransaction&lt;/span>(Event event, Context context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">handle&lt;/span>(Event event, Context context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// migration code goes here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> String &lt;span style="color:#a6e22e">getCallbackName&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;test1&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In this example, &lt;code>supports&lt;/code> is telling Flyway that this callback should only be executed for the event &lt;code>AFTER_EACH_MIGRATE&lt;/code> and if the version that was just applied is &lt;code>3.0.1&lt;/code>. You could also tie back to your SQL file using &lt;code>context.getMigrationInfo().getScript()&lt;/code> if you prefer. This is a quite similar approach to Magento&amp;rsquo;s deprecated UpgradeData classes, although I rather use JavaMigration for this purpose. I can see it being useful though, like running external commands like flushing a cache layer or reporting about the migration if you have observability tooling built into code.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/08/29/java-db-migration/</id><category term="java"/><category term="spring"/></entry><entry><title>First project with Java Spring</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/08/25/first-spring-project/"/><published>2023-08-25T00:00:00Z</published><updated>2023-08-25T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>It has been a long time since I last wrote any meaningful Java code. Aside from some Jenkins plugin debugging here and there, the bulk of my Java experience is from the 2000s, mostly desktop apps (Swing/AWT), so after a nudge from a friend I&amp;rsquo;ve decided to dust off my long forgotten Java skills and I set to rewrite &lt;a href="/projects/ytemail/">YT Email&lt;/a> using Spring as a learning exercise.&lt;/p>
&lt;h2 id="first-impressions">First impressions&lt;/h2>
&lt;p>A big chunk of my software engineering experience is using PHP, that&amp;rsquo;s where it&amp;rsquo;s easier to compare things to, and right from the start I felt right at home, as most of the concepts are also present in Symfony/Doctrine and there is a high grade of translatability between both frameworks. There were a few things that Magento also does similarly to Spring but with different names.&lt;/p>
&lt;p>&lt;a href="https://start.spring.io/">start.spring.io&lt;/a> makes it a breeze easy to bootstrap a new project, similar to the &lt;a href="https://symfony.com/doc/current/setup.html#creating-symfony-applications">Symfony CLI&lt;/a>, although in a web interface. Define your dependencies, extract the zip on your machine and &lt;code>git init&lt;/code> is all you need to be up and running. Ah, and also IntelliJ IDEA, which allowed me to keep my PhpStorm keybindings.&lt;/p>
&lt;h3 id="hello-world">Hello world&lt;/h3>
&lt;p>We start with a simple hello world.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> com.example;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.springframework.stereotype.Controller;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> org.springframework.web.bind.annotation.RequestMapping;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@Controller&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HomeController&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@RequestMapping&lt;/span>(path &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@ResponseBody&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> String &lt;span style="color:#a6e22e">home&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Hello World!&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Comparing that to Symfony:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;?&lt;/span>&lt;span style="color:#a6e22e">php&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">namespace&lt;/span> &lt;span style="color:#a6e22e">App\Controller&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">use&lt;/span> &lt;span style="color:#a6e22e">Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">use&lt;/span> &lt;span style="color:#a6e22e">Symfony\Component\HttpFoundation\Response&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HomeController&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#a6e22e">AbstractController&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">#[Route(&amp;#39;/&amp;#39;, name: &amp;#39;home&amp;#39;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">index&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">Response&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;Hello World!&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>There are a few differences here that I&amp;rsquo;d like to unpack here. First, you need the &lt;code>@ResponseBody&lt;/code> annotation in order to return the contents directly in the controller, otherwise the default will be to render a template with that name available in &lt;code>src/main/resources/templates&lt;/code>. With Symfony, you&amp;rsquo;d return an object that implements the &lt;code>ResponseInterface&lt;/code>, be that a JSON, a redirect or a supertype that handles the template parsing (i.e.: &lt;code>return $this-&amp;gt;render('my/template.html.twig');&lt;/code>). Annoyingly, if you want to redirect in Spring, you have to either &lt;code>return &amp;quot;redirect:/my/new/url&amp;quot;;&lt;/code> or change the method signature to return a &lt;code>org.springframework.web.servlet.view.RedirectView&lt;/code> object, which sounds like it would reduce the flexibility, although I haven&amp;rsquo;t had troubles with it yet.&lt;/p>
&lt;p>The second thing that caught my eye was the &lt;code>@Controller&lt;/code> annotation. In Symfony, the controllers are often placed in the &lt;code>src/controllers&lt;/code> folder, although you can configure a different folder to be scanned (if you&amp;rsquo;re using the &lt;code>#[Route]&lt;/code> annotation) or directly point each route to a method in any PHP class. In Spring, you just give your class the &lt;code>@Controller&lt;/code> annotation and the routes (&lt;code>@RequestMapping&lt;/code> or a subtype) will be parsed from it. This also means you can put your controller anywhere.&lt;/p>
&lt;p>This sounds a bit too magic at first, but the answer lies in the &lt;code>@SpringBootApplication&lt;/code> annotation from your main &lt;code>Application&lt;/code> class (which came in the ZIP file from &lt;a href="https://start.spring.io/">start.spring.io&lt;/a>). &lt;code>@SpringBootApplication&lt;/code> inherits &lt;code>@ComponentScan&lt;/code>, which tells Spring to scan everything in the current package (and sub-packages), and that is true for other types of objects (like your &lt;code>@Configuration&lt;/code> classes can be anywhere). I&amp;rsquo;m assuming this is done at compile-time, so in theory the resulting JAR could have a Map/List/Array with all the controllers named (similar to what &lt;code>composer dump-autoloader&lt;/code> would do, but for controllers/routes/configuration/etc). Even if it doesn&amp;rsquo;t work as I imagine, I still appreciate the flexibility of just using the &lt;code>@Controller&lt;/code> annotation, as it gives me more freedom to organize the code.&lt;/p>
&lt;h3 id="dependency-injection">Dependency Injection&lt;/h3>
&lt;p>It&amp;rsquo;s not quite clear to me yet if it&amp;rsquo;s Java or Spring that provides each feature that I use here, but I do really enjoyed working with dependencies in this project. Both Symfony and Magento also have advanced dependency injection capabilities, so I started with the standard that I was used to, declaring the dependencies in the constructor:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@Controller&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@RequestMapping&lt;/span>(path &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/settings&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Settings&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> MailService mailService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> PasswordEncoder passwordEncoder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> UserRepository userRepository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#a6e22e">Settings&lt;/span>(MailService mailService, PasswordEncoder passwordEncoder, UserRepository userRepository)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">mailService&lt;/span> &lt;span style="color:#f92672">=&lt;/span> mailService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">passwordEncoder&lt;/span> &lt;span style="color:#f92672">=&lt;/span> passwordEncoder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">userRepository&lt;/span> &lt;span style="color:#f92672">=&lt;/span> userRepository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I could also just use the &lt;code>@Autowired&lt;/code> annotation without having them in the constructor, but you will get a NullPointerException if you ever do instantiate the class without using the DI container, so I rather avoid it.&lt;/p>
&lt;p>Similarly to Symfony, you can also declare dependencies in your controller&amp;rsquo;s methods:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@GetMapping&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> String &lt;span style="color:#a6e22e">index&lt;/span>(&lt;span style="color:#a6e22e">@AuthenticationPrincipal&lt;/span> User user)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">//&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That injects the authenticated &lt;code>User&lt;/code> into the controller, ready to be used.&lt;/p>
&lt;h4 id="beans">Beans&lt;/h4>
&lt;p>Another concept that I see here and there are &lt;em>Beans&lt;/em>. My first encounter was with Spring Security, in a situation similar to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@EnableWebSecurity&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@Configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">SecurityConfig&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@Bean&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PasswordEncoder &lt;span style="color:#a6e22e">getPasswordEncoder&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> PasswordEncoderFactories.&lt;span style="color:#a6e22e">createDelegatingPasswordEncoder&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The best comparision I can make is to the &lt;code>di.xml&lt;/code>&amp;rsquo;s &lt;code>&amp;lt;preference&amp;gt;&lt;/code> in &lt;a href="https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/#abstraction-implementation-mappings">Magento&lt;/a>. With &lt;code>@Bean&lt;/code> I&amp;rsquo;m telling Spring&amp;rsquo;s DI container &lt;em>whenever the &lt;code>PasswordEncoder&lt;/code> is to be injected, pass the result of this method&lt;/em>. The method &lt;code>getPasswordEncoder&lt;/code> in the example above will only be called once (unclear to me if once per-request or once during the lifetime of the application), so Spring will just send the same instance of the object returned by the method whenever another class needs a &lt;code>PasswordEncoder&lt;/code> object. Alternatively, this is basically the same as auto-wiring the objects (or interfaces!), but they require some setup to be instantiated. I&amp;rsquo;m also allowed to have a Bean for a &lt;code>ConcreteClass&lt;/code> and it return a subtype of it, but unlike Magento, I can&amp;rsquo;t use a &lt;code>@Bean&lt;/code> to override a class that has already been registered as a Bean somewhere else (i.e.: the &lt;code>@Service&lt;/code> annotation). Maybe there is a package hierarchy involved, but I haven&amp;rsquo;t tested that deeper yet.&lt;/p>
&lt;h3 id="relational-databases">Relational databases&lt;/h3>
&lt;p>Having worked extensively with &lt;a href="https://www.doctrine-project.org/">Doctrine 2&lt;/a>, Hibernate felt like home. Entities, repositories, custom queries and life cycle hooks behave the same, using them in services is straight-forward, the schema can be auto-generated from the entities, etc. However, I do miss something like &lt;a href="https://www.doctrine-project.org/projects/migrations.html">Doctrine Migrations&lt;/a> in the Java world, as both Flyway and Liquibase don&amp;rsquo;t allow me to write any Java code to handle migrations. With Doctrine Migrations, I can write PHP to handle some edge cases in migrations, like this snippet I&amp;rsquo;ve extracted from the &lt;a href="/projects/money/">Money&lt;/a> project:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;?&lt;/span>&lt;span style="color:#a6e22e">php&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">declare&lt;/span>(&lt;span style="color:#a6e22e">strict_types&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">namespace&lt;/span> &lt;span style="color:#a6e22e">DoctrineMigrations&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">use&lt;/span> &lt;span style="color:#a6e22e">Doctrine\DBAL\Schema\Schema&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">use&lt;/span> &lt;span style="color:#a6e22e">Doctrine\Migrations\AbstractMigration&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Version20230825001&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#a6e22e">AbstractMigration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">up&lt;/span>(&lt;span style="color:#a6e22e">Schema&lt;/span> $schema)&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#a6e22e">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $this&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">addSql&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;ALTER TABLE `transaction` ADD COLUMN amount_in_cents BIGINT;&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $updateStmt &lt;span style="color:#f92672">=&lt;/span> $this&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">connection&lt;/span>&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">prepare&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;UPDATE `transaction` SET amount_in_cents = :amount_in_cents WHERE id = :id&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $result &lt;span style="color:#f92672">=&lt;/span> $this&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">connection&lt;/span>&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">fetchAllAssociative&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;SELECT id, amount FROM `transaction`&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">foreach&lt;/span> ($result &lt;span style="color:#66d9ef">as&lt;/span> $item) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $amount &lt;span style="color:#f92672">=&lt;/span> $this&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">convertFromStringToInteger&lt;/span>($item[&lt;span style="color:#e6db74">&amp;#39;amount&amp;#39;&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $updateStmt&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">bindValue&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;id&amp;#39;&lt;/span>, $item[&lt;span style="color:#e6db74">&amp;#39;id&amp;#39;&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $updateStmt&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">bindValue&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;amount_in_cents&amp;#39;&lt;/span>, $amount);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $updateStmt&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">executeQuery&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">down&lt;/span>(&lt;span style="color:#a6e22e">Schema&lt;/span> $schema)&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#a6e22e">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $this&lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">addSql&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;ALTER TABLE `transaction` DROP COLUMN;&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This would have been a nice feature, specially given YT Email already had a database and I wanted to just point the Spring project to the same DB in production. Thankfully I had only a few cases that would need a more elaborated migration, so I just fixed them manually.&lt;/p>
&lt;h3 id="templating">Templating&lt;/h3>
&lt;p>I haven&amp;rsquo;t done much complex templating with Thymeleaf yet, but looking at the documentation it seems as feature-complete as Twig. Its syntax is much more tied to the HTML syntax, so to some extent you could also render the view directly in the browser for a quicker turnaround. I rather have Twig&amp;rsquo;s style though, but this isn&amp;rsquo;t unpleasant to use. To transfer data between the Controller and a Thymeleaf template, you can add attributes to either a &lt;code>org.springframework.ui.Model&lt;/code> or &lt;code>org.springframework.web.servlet.ModelAndView&lt;/code> that you declare in your controller method&amp;rsquo;s signature, i.e.:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@GetMapping&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> String &lt;span style="color:#a6e22e">index&lt;/span>(Model model, &lt;span style="color:#a6e22e">@AuthenticationPrincipal&lt;/span> User user)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model.&lt;span style="color:#a6e22e">addAttribute&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;username&amp;#34;&lt;/span>, user.&lt;span style="color:#a6e22e">getUsername&lt;/span>());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;settings/index&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And to read them in Thymeleaf, &lt;code>&amp;lt;input th:value=&amp;quot;${username}&amp;quot; /&amp;gt;&lt;/code> will do the trick.&lt;/p>
&lt;h2 id="deployment">Deployment&lt;/h2>
&lt;p>Go allows me to deploy to deploy a single file, and that simplifies a lot the deployment process. With Spring Boot, I can do the same (provided I have java installed in the server), or the OCI image is also quite straight-forward:&lt;/p>
&lt;pre tabindex="0">&lt;code>FROM docker.io/library/openjdk:20
COPY build/libs/*.jar /app/app.jar
CMD [&amp;#34;java&amp;#34;, &amp;#34;-jar&amp;#34;, &amp;#34;/app/app.jar&amp;#34;]
&lt;/code>&lt;/pre>&lt;p>Although I love PHP and have used the mainstream options to deploy projects with it, it is unfortunately a lot more involved, especially so that PHARs never took off and it&amp;rsquo;s cumbersome to make them work for web projects. If you are using OCI containers in and out, it won&amp;rsquo;t make much difference. &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-application.application-exit">Spring handles signals for you&lt;/a>, so I don&amp;rsquo;t need &lt;a href="https://github.com/Supervisor/supervisor">supervisord&lt;/a> in my containers anymore.&lt;/p>
&lt;h2 id="outro">Outro&lt;/h2>
&lt;p>In general, I quite enjoyed working with Spring, and will likely build a few more projects with it in the future. I do miss a few tools, however, like Symfony&amp;rsquo;s &lt;a href="https://symfony.com/doc/current/profiler.html">Profiler&lt;/a> and the Web Debug Toolbar, as they give me so much information during development, and the &lt;a href="https://github.com/symfony/maker-bundle/tree/main/src/Maker">SymfonyMakerBundle&lt;/a>, that with a single CLI command auto-generate a bunch of different classes to speed up the process. I haven&amp;rsquo;t really searched for them though, and there might also be IntelliJ extensions that do some parts of their job. All in all, I&amp;rsquo;m looking forward for my next feature/project using Java and Spring!&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/08/25/first-spring-project/</id><category term="java"/><category term="spring"/></entry><entry><title>SSH server behind NAT</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/05/21/ssh-server-behind-nat/"/><published>2023-05-21T00:00:00Z</published><updated>2023-05-21T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I have an always-on Raspberry Pi at home, and once in a while I need to connect to something on my home network, or even exit to the internet as if I were at home (quite handy to access services that block datacenter/country IP ranges). This post documents all the steps needed to make it work.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>My home connection is behind a few of layers of (CG)NAT, so I can&amp;rsquo;t connect to it directly from outside my home network. Instead, I&amp;rsquo;ll be tunnelling through a VPS that I own. This approach consists of two parts: a persistent SSH tunnel between the Raspberry Pi and a VPS, and a connection from my laptop to the Raspberry Pi, through the SSH tunnel.&lt;/p>
&lt;p>This approach isn&amp;rsquo;t quite performant, specially with Raspberry Pi 3B scoring low on SSH performance. I often see the latency increasing four fold through the tunnel (with everything being ~100ms apart of each other), and bandwidth is also expected to be limited (my tests yielded around 20Mbps, with the weakest link capped at 50Mbps). It works alright for my use case, but I wouldn&amp;rsquo;t expect to stream video through the tunnel.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;h3 id="raspberry-pi-generate-ssh-key">Raspberry Pi: generate SSH key&lt;/h3>
&lt;p>Generate a SSH keypair for the &lt;code>root&lt;/code> user of the Raspberry Pi. Copy the public key to configure in the VPS.&lt;/p>
&lt;h3 id="vps-dedidated-user-for-the-tunnel">VPS: dedidated user for the tunnel&lt;/h3>
&lt;p>Assuming you already can SSH into your VPS from anywhere, we now need to allow the Raspberry Pi to create a tunnel to the VPS. I&amp;rsquo;m setting up a dedicated Linux user for this:&lt;/p>
&lt;pre tabindex="0">&lt;code>root@vps# adduser -m -s /usr/sbin/nologin ssh-a1b2c3d4
&lt;/code>&lt;/pre>&lt;p>Then configure the &lt;code>.ssh/authorized_keys&lt;/code> of this user:&lt;/p>
&lt;pre tabindex="0">&lt;code>command=&amp;#34;/usr/sbin/nologin&amp;#34;,port-forwarding,permitlisten=&amp;#34;127.0.0.1:50001&amp;#34;,no-pty ssh-public-key-goes-here
&lt;/code>&lt;/pre>&lt;p>The extra parameters at the beginning make sure the user can only setup the tunnel and forward ports, but has no access to a shell.&lt;/p>
&lt;h3 id="raspberry-pi-setup-the-persistent-tunnel">Raspberry Pi: setup the persistent tunnel&lt;/h3>
&lt;p>I&amp;rsquo;m using &lt;a href="https://linux.die.net/man/1/autossh">autossh&lt;/a>, as it monitor the connection and restarts if it becomes stale. Quite handy if your home connection isn&amp;rsquo;t stable. To make it persistent across reboots, I&amp;rsquo;m setting it up as a systemd unit:&lt;/p>
&lt;pre tabindex="0">&lt;code># cat /etc/systemd/system/sshtunnel.service
[Unit]
Description=SSH Tunnel
After=network.target
[Service]
Restart=always
RestartSec=20
User=root
ExecStart=/usr/bin/autossh -M 0 -nNT -o ServerAliveInterval=120 -R 127.0.0.1:50001:localhost:22 ssh-a1b2c3d4@vps-ip-address
[Install]
WantedBy=multi-user.target
&lt;/code>&lt;/pre>&lt;p>This will bind the port &lt;code>50001&lt;/code> in the VPS to the port &lt;code>22&lt;/code> of the Raspberry Pi.&lt;/p>
&lt;p>Don&amp;rsquo;t forget to run &lt;code>systemctl enable sshtunnel&lt;/code> and &lt;code>systemctl start sshtunnel&lt;/code>.&lt;/p>
&lt;h2 id="usage">Usage&lt;/h2>
&lt;p>To make use of the tunnel, run:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ ssh -J vps-user@vps.ip.goes.here -p 50001 pi-user@127.0.0.1
&lt;/code>&lt;/pre>&lt;p>This will connect to the pi-user in the Raspberry Pi, via the SSH tunnel we set up earlier. To use the same tunnel as a SOCKS5 proxy, so I can exit to the internet as if I was home, use:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ ssh -J vps-user@vps.ip.goes.here -p 50001 pi-user@127.0.0.1 -D 1337 -q -C -N
&lt;/code>&lt;/pre>&lt;p>This opens a SOCKS5 proxy on port &lt;code>1337&lt;/code> on my computer, which then I can configure in the browser/system.&lt;/p>
&lt;h3 id="sshconfig">.ssh/config&lt;/h3>
&lt;p>To simplify usage it&amp;rsquo;s possible to run &lt;code>ssh pi-home&lt;/code> and connect to it from anywhere. Add those lines to the &lt;code>.ssh/config&lt;/code> of the computer that will access the Raspberry Pi:&lt;/p>
&lt;pre tabindex="0">&lt;code>Host vps
HostName vps.ip.goes.here
User vps-user
Host pi-home
HostName 127.0.0.1
Port 50001
User pi-user
ProxyJump vps
&lt;/code>&lt;/pre>&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/05/21/ssh-server-behind-nat/</id><category term="linux"/><category term="network"/></entry><entry><title>Reusing old hardware for self-hosting</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/05/16/reusing-old-hardware/"/><published>2023-05-16T00:00:00Z</published><updated>2023-05-16T00:00:00Z</updated><content type="html">&lt;p>Hi!&lt;/p>
&lt;p>I used to self-host a NAS in my home network out of a Raspberry Pi 4B and a couple of HDDs, but after a few years I&amp;rsquo;ve learned that the Pi is &lt;a href="/en/2022/03/16/update-to-my-zfs-backup-strategy/">somewhat underpowered&lt;/a> for my needs, getting in the way of my backup strategy. Once in a while I would search for NUCs and other small form factor computers, but I never found something in the range that I was comfortable to pay. Eventually, however, something clicked: I had the right solution under my nose the entire time: a &lt;a href="https://www.notebookcheck.net/Review-Dell-Vostro-3450-Notebook.55189.0.html">2011 Dell Vostro 3450&lt;/a>.&lt;/p>
&lt;p>&lt;a href="/images/2023-05-old-hardware/overview.jpg">&lt;img alt="Dell Vostro 3450, front side overview, with a Frozen sticker" loading="lazy" src="/images/2023-05-old-hardware/overview.jpg">&lt;/a>&lt;/p>
&lt;p>My partner had purchased this device for her college years and it had reasonably beaten up when she graduated, so it was retired and sat in a closet ever since. Apart from the bend in the body and a few keys not working (notably the Ctrl and Alt keys), the display is also somewhat loose and the right side ports (HDMI, USB, eSATA!?) are shorted, so the system would sometimes reboot or display garbage when messing with the USB port. I also got a few a small discharges that I initially attributed to low humidity ESD (somewhat frequent for me with any hardware), but thankfully I didn’t lose any USB devices until I realized what was going on. Covering the ports with electrical tape was a quick and cheap solution.&lt;/p>
&lt;p>The 2.3GHz i3-2350M with 4GB DDR3 isn&amp;rsquo;t particularly better than the Raspberry Pi 4B in the benchmarks, but given my use case could see better single-core performance (Nextcloud image resizing kept me waiting) and it had cryptographic acceleration, I figured it was worth the try. I installed Debian, configured BTRFS (I&amp;rsquo;m using btrfs snapshots for incremental backups) and it quickly became part of my local infrastructure.&lt;/p>
&lt;p>The way I use my NAS isn&amp;rsquo;t quite standard, however. I initially went with Nextcloud as a Google Photos replacement, and for me it meant mostly archival, so the initial task was just backing up my phone&amp;rsquo;s photos. I&amp;rsquo;d turn on the NAS, run the sync, run the incremental backup scripts and shut it down. I eventually started using the file sync with a couple of computers at home, but was still using a free Dropbox account for day-to-day small things. If I edited something from my Nextcloud folder in a computer, I&amp;rsquo;d likely wait a few days until it was &amp;ldquo;worth the hassle&amp;rdquo; to turn the NAS on again, and at this point I was making the same &lt;em>requires willpower&lt;/em> mistakes once again.&lt;/p>
&lt;p>Eventually I wanted to bring home more services, like LinkAce, FreshRSS and Gitea, so I decided to transform it into an always-on solution. First I had to address the spinning rust noise, as I caught myself avoiding turning the NAS on just because of the low hum it made in the background. To fix this I purchased an 1TB SSD and went on to replace it, just to realize that the disk was the last thing I could remove:&lt;/p>
&lt;p>&lt;a href="/images/2023-05-old-hardware/underside.jpg">&lt;img alt="Dell Vostro 3450, upside down, with the service cover removed, exposing the memory slots and part of the hard to access HDD" loading="lazy" src="/images/2023-05-old-hardware/underside.jpg">&lt;/a>&lt;/p>
&lt;p>&lt;a href="/images/2023-05-old-hardware/disassembled.jpg">&lt;img alt="Dell Vostro 3450, fully disassembled with the motherboard upside down, exposing the HDD" loading="lazy" src="/images/2023-05-old-hardware/disassembled.jpg">&lt;/a>&lt;/p>
&lt;p>Given I had it open and fully disassembled at this point, and also because of the shorted ports and other rusty parts in the motherboard, I second guessed myself and decided to not try with a brand new SSD before making sure I didn&amp;rsquo;t screw it up. So I bought some cleaning supplies (a can of compressed air, isopropanol, thermal paste), scavenged a 240GB SSD and a pair of 4GB DDR3 DIMMs out of a 2009 MacBook Pro and did a thorough cleaning and reassembly.&lt;/p>
&lt;p>It booted! Debian was then installed, I ran my Ansible playbook and synced back my Nextcloud BTRFS snapshots into the SSD. The baseload uses around 300MB RAM and is silent, at 47°C it can be mistaken for a fanless system, as I only ever hear the fan when I&amp;rsquo;m browsing through Nextcloud&amp;rsquo;s gallery and it needs to generate thumbnails. It idles at around 12.6W with the screen on, and I have seen peaks of 38W with a couple of USB HDDs connected and a lot of load. 12W is quite low, but still 2.5 times more than a Pi, and I thought there was room for improvement.&lt;/p>
&lt;p>The first win came out of the &lt;code>consoleblank&lt;/code> Linux kernel parameter. I configured it to 60 seconds, and after the timeout it completely shuts off the display, bringing the idle consumption to 8.1W. While 8W amounts to about 30 SEK/month with my current electricity contract, I&amp;rsquo;m not particularly fond of wasting energy, but fortunately I got lucky as this machine had one last surprise hidden for me: full Wake on LAN support! I did some initial tests with WOL using Magic packets and it worked flawlessly. The system in the suspended state consumes just 1.1W and a magic packet is enough to wake it back to fully functional in less than 5 seconds. One problem I&amp;rsquo;m still looking for a fix is that the &lt;code>consoleblank&lt;/code> that does not turn off the display once the system resumed from suspend.&lt;/p>
&lt;p>So my next challenges in this project are all in software. I want to automatically suspend the system based on my usage patterns. I also want to avoid the &lt;em>requires willpower&lt;/em> pitfall and have something awake the system when there is usage, but I suspect using the WOL unicast mode might conflict with the Nextcloud desktop app trying to stay connected. Alternatively, I can have my home DNS server (an always-on Raspberry Pi 3B) to send the magic packet when it detects usage.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/05/16/reusing-old-hardware/</id><category term="hardware"/><category term="linux"/><category term="network"/></entry><entry><title>Understanding Go URL handling</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2023/03/24/understanding-go-url-handling/"/><published>2023-03-24T00:00:00Z</published><updated>2023-03-24T00:00:00Z</updated><content type="html">&lt;p>Hi.&lt;/p>
&lt;p>A couple of years ago I &amp;ldquo;took inspiration&amp;rdquo; for a HTTP reverse proxy in Go from Stack Overflow without putting too much thought into it, and this week it bit me back. A co-worker found out that it was normalising some URLs (&lt;code>/something//else&lt;/code> will 301-redirect to &lt;code>/something/else&lt;/code>) against their will. So I decided to take the opportunity and understand better how &lt;code>net/http&lt;/code> handles URLs, and here are my findings.&lt;/p>
&lt;h2 id="minimal-go-http-server">Minimal Go HTTP Server&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ListenAndServe&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;:8080&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This simplest HTTP server you can write in Go. Given we&amp;rsquo;re providing the handler argument with &lt;code>nil&lt;/code>, it won&amp;rsquo;t do much other than open the TCP port and return 404 for any URL you request. While that&amp;rsquo;s not particularly useful, is enough to make sure we&amp;rsquo;re able to compile, run and make requests.&lt;/p>
&lt;p>However, it&amp;rsquo;s more likely that you&amp;rsquo;ll want the server to show some content, so for that you could write:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">HandleFunc&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;/&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">writer&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ResponseWriter&lt;/span>, &lt;span style="color:#a6e22e">request&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">writer&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>([]byte(&lt;span style="color:#e6db74">&amp;#34;Hello, world&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ListenAndServe&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;:8080&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And sure enough, it works! Requesting &lt;code>/&lt;/code> returns &lt;code>Hello, world&lt;/code>. But so does requesting &lt;code>/something&lt;/code>. Hum. Why is that?&lt;/p>
&lt;p>The first argument in &lt;code>HandleFunc&lt;/code> is &lt;code>pattern&lt;/code>, which will start matching at the beginning of the URL. So &lt;code>http.HandleFunc(&amp;quot;/test/&amp;quot;, ...)&lt;/code> will return 200 for &lt;code>/test/&lt;/code> and &lt;code>/test/another/&lt;/code>, but 404 for &lt;code>/another/test/&lt;/code>. However, &lt;code>http.HandleFunc(&amp;quot;/test&amp;quot;, ...)&lt;/code> (without the trailing slash) will only match &lt;code>/test&lt;/code>, and will return 404 for both &lt;code>/test/&lt;/code> and &lt;code>/test/another&lt;/code>. So ending the pattern with &lt;code>/&lt;/code> has some special handling.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">http.HandleFunc(pattern)&lt;/th>
&lt;th style="text-align: left">Requested URL&lt;/th>
&lt;th style="text-align: left">HTTP status&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/another&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/another/&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/another&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/another/test&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/another/test/&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test&lt;/code>&lt;/td>
&lt;td style="text-align: left">301 to &lt;code>/test/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/test/another&lt;/code>&lt;/td>
&lt;td style="text-align: left">200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/another/test&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>&amp;quot;/test/&amp;quot;&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>/another/test/&lt;/code>&lt;/td>
&lt;td style="text-align: left">404&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;td style="text-align: left">&amp;ndash;&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>So how do I match &lt;code>/&lt;/code> and only &lt;code>/&lt;/code>? Let&amp;rsquo;s try with an empty string for &lt;code>http.HandleFunc&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ go run main.go
panic: http: invalid pattern
goroutine 1 [running]:
net/http.(*ServeMux).Handle(0x14622e0, {0x0, 0x0}, {0x12e7c80?, 0x12a8380})
/usr/local/Cellar/go/1.20.2/libexec/src/net/http/server.go:2510 +0x25f
net/http.(*ServeMux).HandleFunc(...)
/usr/local/Cellar/go/1.20.2/libexec/src/net/http/server.go:2553
net/http.HandleFunc(...)
/usr/local/Cellar/go/1.20.2/libexec/src/net/http/server.go:2565
main.main()
/Users/kassner/workspace/test/main.go:6 +0x33
exit status 2
&lt;/code>&lt;/pre>&lt;p>It doesn&amp;rsquo;t compile. Digging into it I found out that it&amp;rsquo;s actually &lt;a href="https://github.com/golang/go/issues/4799">not possible&lt;/a> using the default behaviour. But as it was pointed out in the thread:&lt;/p>
&lt;blockquote>
&lt;p>The ServeMux isn&amp;rsquo;t fundamental to the net/http package. If you don&amp;rsquo;t like its behavior, you can write your own mux.&lt;/p>&lt;/blockquote>
&lt;h2 id="terminology-is-hard">Terminology is hard&lt;/h2>
&lt;p>So what is a ServeMux? Quoting from &lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.20.2:src/net/http/server.go;l=2271-2305">Go&amp;rsquo;s source&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.&lt;/p>&lt;/blockquote>
&lt;p>I have known that as a &lt;em>router&lt;/em> from some Web Frameworks (Express, Spring, Symfony). All it does is to match a particular URL to an arbitrary piece of code that I declared. Sounds what I need, right?&lt;/p>
&lt;p>Turns out the term &lt;code>ServeMux&lt;/code> only confused me. What I needed is a &lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.20.2:src/net/http/server.go;l=62">Handler&lt;/a>, and ServeMux is only one implementation of a handler that comes baked into Go. Of course, in some situations ServeMux might be enough, I just wish it was clearer to me it was a &lt;em>&lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.20.2:src/net/http/server.go;l=2917-2919">DefaultHandler&lt;/a>&lt;/em>.&lt;/p>
&lt;p>It also didn&amp;rsquo;t help me that Go has both &lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.20.2:src/net/http/server.go;l=2114-2123">http.HandlerFunc&lt;/a> and &lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.20.2:src/net/http/server.go;l=2564-2566">http.HandleFunc&lt;/a>. The former is used to wrap a &lt;code>func&lt;/code> into Handler, which is what we need to create our own Handler, and the latter does something similar, but registering the function with a pattern matching against the DefaultServeMux. A simple typo can cause you a good amount of head-scratching.&lt;/p>
&lt;p>I have a preference for verbose and explicit intent, so I find this more comprehensive to read:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">DefaultServeMux&lt;/span>.&lt;span style="color:#a6e22e">HandleFunc&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;/&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">writer&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ResponseWriter&lt;/span>, &lt;span style="color:#a6e22e">request&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">writer&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>([]byte(&lt;span style="color:#e6db74">&amp;#34;Hello, world&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ListenAndServe&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;:8080&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That way it&amp;rsquo;s explicit that I&amp;rsquo;ll be using the &lt;del>DefaultHandler&lt;/del>ServeMux provided by Go, with all the behaviours that come out of the box.&lt;/p>
&lt;h2 id="crafting-a-handler">Crafting a Handler&lt;/h2>
&lt;p>So, if a &lt;code>Handler&lt;/code> is all we need, how do we rewrite our server using it?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">handler&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">HandlerFunc&lt;/span>(&lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">writer&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ResponseWriter&lt;/span>, &lt;span style="color:#a6e22e">request&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">writer&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>([]byte(&lt;span style="color:#e6db74">&amp;#34;Hello, world&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ListenAndServe&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;:8080&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">handler&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As we didn&amp;rsquo;t add any logic to it, any URL will return &lt;code>Hello, world&lt;/code>. More importantly though, a request with double-slashes, which was my original problem, will not be normalized, which confirms we&amp;rsquo;re overriding the default behaviour.&lt;/p>
&lt;pre tabindex="0">&lt;code>$ curl -v http://localhost:8080/something//else
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
&amp;gt; GET /something//else HTTP/1.1
&amp;gt; Host: localhost:8080
&amp;gt; User-Agent: curl/7.79.1
&amp;gt; Accept: */*
&amp;gt;
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 200 OK
&amp;lt; Date: Fri, 24 Mar 2023 06:01:38 GMT
&amp;lt; Content-Length: 12
&amp;lt; Content-Type: text/plain; charset=utf-8
&amp;lt;
* Connection #0 to host localhost left intact
Hello, world
&lt;/code>&lt;/pre>&lt;p>And if I want to match just &lt;code>/&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">handler&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">HandlerFunc&lt;/span>(&lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">writer&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ResponseWriter&lt;/span>, &lt;span style="color:#a6e22e">request&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">request&lt;/span>.&lt;span style="color:#a6e22e">URL&lt;/span>.&lt;span style="color:#a6e22e">Path&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">writer&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>([]byte(&lt;span style="color:#e6db74">&amp;#34;Hello, world&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">NotFound&lt;/span>(&lt;span style="color:#a6e22e">writer&lt;/span>, &lt;span style="color:#a6e22e">request&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">ListenAndServe&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;:8080&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">handler&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="conclusions">Conclusions&lt;/h2>
&lt;p>My main takeaway is that I need to be more careful with assumptions and read the source/manual more often, instead of just assume things with the same name work similarly across languages. Going through the issue also led me to read Go&amp;rsquo;s source code, which I honestly expected to be a lot harder than my previous experiences, mainly because it&amp;rsquo;s written in the same language. Lastly, it also showed me how a developer could take some shortcuts that would not please my explicit intent preferences, so I can be aware of those gotchas in the future. I&amp;rsquo;m glad I&amp;rsquo;ve done this.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2023/03/24/understanding-go-url-handling/</id><category term="go"/><category term="http"/></entry><entry><title>Update to my ZFS backup strategy</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2022/03/16/update-to-my-zfs-backup-strategy/"/><published>2022-03-16T00:00:00Z</published><updated>2022-03-16T00:00:00Z</updated><content type="html">&lt;p>I briefly outlined in my &lt;a href="/en/2020/12/19/zfs-backup-strategy/">ZFS backup strategy blogpost&lt;/a> about my NAS setup, but here it&amp;rsquo;s a quick recap: I have a Raspberry Pi 4 4GB with a 1TB SATA HDD over USB running under the TV in my living room, and a second USB HDD for mirroring. I&amp;rsquo;ve been running this setup for around 18 months now, and unfortunately it doesn&amp;rsquo;t quite fit my needs.&lt;/p>
&lt;p>In the previous post, I focused too much in the remote/cloud backups for ZFS, so I just took it for granted that mirroring the disks would be trivial using ZFS. While ZFS does mirroring by default, now I understand that it&amp;rsquo;s intended as a solution for always-online disks, so I couldn&amp;rsquo;t rely on that feature without ZFS constantly nagging that the zpool is unhealthy and resilvering the disk every time I plugged it in. To get around that, I&amp;rsquo;ve decided to keep the zpool with a single disk, and &lt;code>zfs send&lt;/code> the data to the second disk once every &lt;em>when I felt like&lt;/em>, mostly because once the disks were fully synced, the delta between the second disk and what&amp;rsquo;s stored on the cloud would be quite small (&amp;lt;100MB).&lt;/p>
&lt;p>That was a rookie mistake. Not only it meant I wouldn&amp;rsquo;t have 3 copies of the data anymore (or at least for the data changes from the last full sync), the data sync between those two disks was a complete disaster. I didn&amp;rsquo;t do a proper research and did my initial sync only after I had already commited to this strategy and had moved in all my data. This meant that I failed to test a pretty trivial thing: can the Raspberry Pi power two USB HDDs at once? It can&amp;rsquo;t. It will boot fine with one disk, but it won&amp;rsquo;t boot with two, and plugging the second disk after it&amp;rsquo;s fully booted and idling still wasn&amp;rsquo;t enough for the second disk to boot.&lt;/p>
&lt;p>With that limitation, I told myself that it would be fine if I plugged the second disk in my Lenovo notebook and did a sync over the network. My home network is gigabit capable, and the syncs wouldn&amp;rsquo;t be that big after the initial full sync, plus it meant that I&amp;rsquo;d have ZFS on Linux already setup in my Fedora installation, in case the main disk went bust and I&amp;rsquo;d need quick access to the files. I wrote a script for &lt;code>ssh nas zfs send | zfs recv&lt;/code> and, oh my, how that was slow. The Raspberry Pi does not have any cryptographic hardware acceleration, so the transfer speed was capped to ~16MB/s, which meant several hours for the initial sync.&lt;/p>
&lt;p>I&amp;rsquo;ve tried to help the Raspberry Pi a bit by doing a non-encrypted transfer with &lt;code>zfs send | nc&lt;/code> and &lt;code>nc -l&lt;/code> over a direct cable connection between the Pi and the notebook, and I got close to 85MB/s in that scenario, which was a lot more palatable. However, it also meant a tripping hazard (a.k.a.: cable) spanning from my living room to my office for a few hours, so I began to regret my decisions. But at least now the disks were synced and subsequential syncs were manageable over SSH, as the deltas were small.&lt;/p>
&lt;p>Enter kernel updates.&lt;/p>
&lt;p>Fast forward 3 months, I&amp;rsquo;m back from a holiday abroad in which I took my NAS server with me (but not the second disk). I kept the NAS server up to date, and ZFS on Linux mostly worked across updates (although it was a bit of a pain between ZOL 0.8.4 and 2.0.0). Given my Lenovo notebook is my main personal machine, I use Fedora because of the speed packages get updated. This also meant, after I was back from vacation, that I had the 5.16 Kernel running in it, but ZFS on Linux&amp;rsquo;s support for it wouldn&amp;rsquo;t come for another 2 months.&lt;/p>
&lt;blockquote>
&lt;p>Sidenote: this post is in no way a critique to the ZFS on Linux project. Using ZFS at all is only possible because of their work, and I understand well enough the problems with unpaid Open Source contributors and &amp;ldquo;AS IS&amp;rdquo; and &amp;ldquo;no guarantees&amp;rdquo; open source licenses. I&amp;rsquo;m thankful for the multiple authors of OpenZFS and ZOL that made this possible, and all the faults on my strategy are solely my own.&lt;/p>&lt;/blockquote>
&lt;p>This mean my strategy fell into pieces. My initial expectation was that I&amp;rsquo;d just plug the second disk to the NAS server every second week or so and it would automagically keep both disks mirrored. In practice, I had to constantly make effort into keeping both disks in sync, plugging into a second machine, fiddling around with some commands and a bit of lack of preparation made me resync the disk once again because I screwed up some parameters in the &lt;code>zfs recv&lt;/code>.&lt;/p>
&lt;p>ZOL 2.1.3 was released last week with 5.16 Kernel support, but I haven&amp;rsquo;t got around to sync the disks again, and now they are unsynced for about 4 months. I kept the snapshots in the primary disk, so I should be able to do an incremental sync whenever I get around it, but the situation I&amp;rsquo;ve put myself doesn&amp;rsquo;t get me excited to jump into that problem. Also, this doesn&amp;rsquo;t mean it won&amp;rsquo;t happen again, so I have to start from scratch and develop a new strategy that considers both cloud incremental backups as much as mirroring to a secondary disk that will not stay connected all the time. Maybe ZFS is not the answer for me, maybe Raspberry Pi is not the answer for me, maybe ZOL is not the answer for me. I don&amp;rsquo;t know yet.&lt;/p>
&lt;p>The good part of this experiment is that I&amp;rsquo;ve got to learn what not to do. So for my future self, a small summary of learnings about this experiment:&lt;/p>
&lt;ul>
&lt;li>Consider the impacts of bleeding edge package updates for infrastructure services;&lt;/li>
&lt;li>The server should be capable of executing the entire backup strategy on its own;&lt;/li>
&lt;li>Consider the power requirements of your system;&lt;/li>
&lt;li>Servers should have cryptographic hardware acceleration;&lt;/li>
&lt;li>Don&amp;rsquo;t take for granted trivial parts of the backup processes and validate the entire model before commiting to it;&lt;/li>
&lt;li>Manual steps in a repetitive process must take into consideration your willpower to deal with it;&lt;/li>
&lt;li>Under no circumstance assume your partner will be happy with a UTP cable crossing the living room;&lt;/li>
&lt;/ul>
&lt;p>If you do have any suggestions for any of my downfalls in this project, I&amp;rsquo;ll appreciate the feedback.&lt;/p>
&lt;p>Thank you.&lt;/p></content><id>https://www.kassner.com.br/en/2022/03/16/update-to-my-zfs-backup-strategy/</id><category term="linux"/></entry><entry><title>Lenovo notebook updates</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2022/03/01/lenovo-notebook-updates/"/><published>2022-03-01T00:00:00Z</published><updated>2022-03-01T00:00:00Z</updated><content type="html">&lt;p>It has now been close to half a decade since acquisition of my &lt;a href="https://www.webhallen.com/se/product/264836-Lenovo-Y720-15IKB-15-6-FHD-IPS-i7-7700HQ-16GB-512GB-SSD-M-2-GTX-1060-6GB-Win-10-R#p-info">Lenovo Y720-15IBK&lt;/a>, so I thought about writing an update this machine, given it is still my main personal machine, seeing almost daily usage for general browsing and a bit of programming. I also game on it semi-regularly, having played a good deal of &lt;a href="https://store.steampowered.com/app/1293830/Forza_Horizon_4/">Forza Horizon 4&lt;/a> (Ultra @ 1080p) and &lt;a href="https://store.steampowered.com/app/227300/Euro_Truck_Simulator_2/">ETS2&lt;/a> over this holidays season.&lt;/p>
&lt;h2 id="hardware">Hardware&lt;/h2>
&lt;p>Upgrading to a Kingston A2000 was the only hardware change I had done in this machine, mostly because I needed 1TB of storage. I haven&amp;rsquo;t seen the need for an upgrade to a better machine, as I can still run the games I like to play (disclaimer: I only have 60Hz displays and I don&amp;rsquo;t care much about graphics quality anyway) and all my programming happens on Linux (no VMs), so the i7-7700HQ should be plenty for a little while.&lt;/p>
&lt;p>Unfortunately, opening the back aluminium case to replace the M.2 disk seems to have affected the touchpad negatively. It was never an outstanding touchpad, I often had to click more than once to produce a click with it, but I feel it became a little worse and the percentage of misclicks went up a bit. The keyboard also had its quirks since purchase (I missed the return window and was lazy to send it to Lenovo to investigate), but given I use it mostly attached to a external screen/keyboard/mouse, it&amp;rsquo;s not a big deal.&lt;/p>
&lt;p>The battery always felt it wasn&amp;rsquo;t enough given the machine has a NVIDIA GTX 1060, but it didn&amp;rsquo;t degrade much so far. I can still squeeze a good couple of hours out of it when running Linux, which I consider a huge feat for the size of the machine.&lt;/p>
&lt;h2 id="fedora">Fedora&lt;/h2>
&lt;p>I haven&amp;rsquo;t switched away from Fedora since my attempt back in 2018. I&amp;rsquo;m now on Fedora 34 and most of the quirks from my &lt;a href="/en/2018/06/27/fedora-28-on-lenovo-y720-15ibk/">Fedora blogpost&lt;/a> are long gone. Eventually the NVIDIA drivers from &lt;a href="https://rpmfusion.org/Howto/NVIDIA#Current_GeForce.2FQuadro.2FTesla">RPM Fusion&lt;/a> became a &amp;ldquo;it just works&amp;rdquo; approach, and given I have no need for a dual-GPU setup, I&amp;rsquo;ve been using them ever since. Secure Boot was neglected though, since the new M.2 disk I&amp;rsquo;ve just ignored it completely, as the reinstall was done in a rush before travels.&lt;/p>
&lt;h2 id="keyboard-backlight">Keyboard backlight&lt;/h2>
&lt;p>For years I accepted that the keyboard backlight wouldn&amp;rsquo;t work with Linux. Last year I&amp;rsquo;ve found &lt;a href="https://github.com/Izurii/Lenovo-Y720-KB-Led-Controller">Izurii/Lenovo-Y720-KB-Led-Controller&lt;/a>, which is an Electron-based app to emulate what the Lenovo Nerve Sense for Windows did for the keyboard backlight. And it works, but given Electron is such a resource hog compared to a single CLI tool, it didn&amp;rsquo;t made sense to keep it.&lt;/p>
&lt;p>I had a day off work this week and decided to play around a bit. I reviewed what the Electron app did and it&amp;rsquo;s core was just a bunch of &lt;code>ioctl&lt;/code> system calls with the right payload. I eventually made it work with static colors and decided that was enough, only to realize that someone already &lt;a href="https://github.com/threadexio/Legion-Y720-Keyboard-Backlight">rewrote the app&lt;/a> in a more old-school Linux friendly way. Alas, at least I dusted off my C skills and still got a tool with all the configs!&lt;/p>
&lt;p>&amp;ndash;&lt;/p>
&lt;p>In general, I&amp;rsquo;m still happy with the machine, I feel like I got out of it what I paid for and thankfully it is still a decent machine that will see several more years of service.&lt;/p></content><id>https://www.kassner.com.br/en/2022/03/01/lenovo-notebook-updates/</id><category term="linux"/></entry><entry><title>Create write-only keys for Backblaze B2</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2021/09/05/b2-write-only-key/"/><published>2021-09-05T00:00:00Z</published><updated>2021-09-05T00:00:00Z</updated><content type="html">&lt;p>As far as I remember, you can&amp;rsquo;t create a write-only key via Backblaze&amp;rsquo;s dashboard without also giving read access to the key. I want to use this specifically for uploaders in servers, so, if compromized, an attacker can&amp;rsquo;t read data out of the bucket.&lt;/p>
&lt;pre tabindex="0">&lt;code>$ curl https://api.backblazeb2.com/b2api/v2/b2_authorize_account -u &amp;#34;MASTER_KEY_ID:MASTER_KEY_SECRET&amp;#34;
{
&amp;#34;apiUrl&amp;#34;: &amp;#34;https://api003.backblazeb2.com&amp;#34;,
&amp;#34;authorizationToken&amp;#34;: &amp;#34;.....&amp;#34;,
}
&lt;/code>&lt;/pre>&lt;p>Replace &lt;code>apiUrl&lt;/code> and &lt;code>authorizationToken&lt;/code> in the next command:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ curl https://$apiUrl/b2api/v2/b2_create_key -d &amp;#39;{&amp;#34;capabilities&amp;#34;: [&amp;#34;listBuckets&amp;#34;,&amp;#34;writeFiles&amp;#34;],&amp;#34;keyName&amp;#34;:&amp;#34;key-name&amp;#34;,&amp;#34;accountId&amp;#34;:&amp;#34;MASTER_KEY_ID&amp;#34;}&amp;#39; -H &amp;#39;Authorization: $authorizationToken&amp;#39;
{
&amp;#34;accountId&amp;#34;: &amp;#34;0f0f0f0f0f0f&amp;#34;,
&amp;#34;applicationKey&amp;#34;: &amp;#34;K....&amp;#34;,
&amp;#34;applicationKeyId&amp;#34;: &amp;#34;00....&amp;#34;,
&amp;#34;bucketId&amp;#34;: null,
&amp;#34;capabilities&amp;#34;: [
&amp;#34;listBuckets&amp;#34;,
&amp;#34;writeFiles&amp;#34;
],
&amp;#34;expirationTimestamp&amp;#34;: null,
&amp;#34;keyName&amp;#34;: &amp;#34;key-name&amp;#34;,
&amp;#34;namePrefix&amp;#34;: null,
&amp;#34;options&amp;#34;: [
&amp;#34;s3&amp;#34;
]
}
&lt;/code>&lt;/pre>&lt;p>That&amp;rsquo;s all.&lt;/p></content><id>https://www.kassner.com.br/en/2021/09/05/b2-write-only-key/</id><category term="linux"/><category term="backblaze"/></entry><entry><title>My ZFS backup strategy</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2020/12/19/zfs-backup-strategy/"/><published>2020-12-19T00:00:00Z</published><updated>2020-12-19T00:00:00Z</updated><content type="html">&lt;p>I am now doing some experiments running my own NAS at home (mostly out of boredom), and I went with a small solution that goes inside my &lt;a href="https://www.ikea.com/se/sv/p/ikea-ps-skap-vit-10251451/">IKEA PS&lt;/a> with a Raspberry Pi 4, a couple of 1TB USB SATA disks and &lt;a href="https://zfsonlinux.org/">ZFS on Linux&lt;/a> mirroring them. I have less than 200GB in data and a very stable 50Mbps uplink at home, so this post explains my strategy to backup my data in a remote location.&lt;/p>
&lt;p>Before I even started, I had to decide what would be my backup strategy. Given I&amp;rsquo;m running this from a home connection, it wasn&amp;rsquo;t an option to do full backups every day, specially when my data rarely changes more than 50MB in a given day, so I had to think about something that would play nice with incremental backups.&lt;/p>
&lt;p>I first researched tools for such tools, and was amazed by &lt;a href="https://www.borgbackup.org/">Borg&lt;/a>, but given I planned to backup this directly to an S3/GCS/B2 bucket, I found too many limitations on it. I can&amp;rsquo;t have the &lt;code>borg&lt;/code> binary running in the remote, so I either needed a full copy of the data locally or it had to download a lot of data from the bucket to be able to compute the incremental diff.&lt;/p>
&lt;p>My second test was with ZFS. Seeing demos of &lt;code>zfs send | ssh remote zfs recv&lt;/code> was very beautiful, but again, I won&amp;rsquo;t have a way to run ZFS on the remote. But I did a few tests with it that caught my attention. First, snapshot management in ZFS is awesome to manage changes across time. Second, you can pipe &lt;code>zfs send&lt;/code> to a file. Third, while &lt;code>zfs send&lt;/code> is commonly used to send a snapshot to another machine also running ZFS (&lt;code>zfs send | ssh remote zfs recv&lt;/code>), when you pipe &lt;code>zfs send&lt;/code> to a non-ZFS command (or file), it will dump &lt;em>the entire filesystem&lt;/em>, essentially creating a full backup. Fourth, you can &lt;code>zfs send&lt;/code> just the diff between two snapshots, even to a file. So I put all of them together to create this backup strategy.&lt;/p>
&lt;h2 id="backup-strategy">Backup strategy&lt;/h2>
&lt;p>Note: the command &lt;code>backup-upload&lt;/code> below is an unpublished tool that will compress, encrypt and upload the file to a remote location.&lt;/p>
&lt;p>&lt;strong>Creating the full backup&lt;/strong>&lt;/p>
&lt;p>I&amp;rsquo;ve imported some data and then created a snapshot with &lt;code>zfs snapshot data@&amp;lt;date&amp;gt;&lt;/code> (i.e.: &lt;code>zfs snapshot data@2020-08-24&lt;/code>). This snapshot was then uploaded to a remote location using &lt;code>zfs send data@2020-08-24 | backup-upload full/2020-08-24&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Creating daily backups&lt;/strong>&lt;/p>
&lt;p>Every day, a cronjob creates a new snapshot and uploads to remote using &lt;code>zfs send -I &amp;lt;yesterdaySnapshot&amp;gt; &amp;lt;todaySnapshot&amp;gt; | backup-upload daily/&amp;lt;date&amp;gt;&lt;/code>).&lt;/p>
&lt;p>&lt;strong>Creating monthly backups&lt;/strong>&lt;/p>
&lt;p>Once a month, after the daily snapshot was created, a cronjob uploads it using &lt;code>zfs send -I &amp;lt;fullbackup&amp;gt; &amp;lt;todaySnapshot&amp;gt; | backup-upload monthly/&amp;lt;date&amp;gt;&lt;/code>).&lt;/p>
&lt;p>This cronjob will also mark for deletion older monthly and daily backups, except the full backup.&lt;/p>
&lt;h2 id="restoration-strategy">Restoration strategy&lt;/h2>
&lt;p>Backups are worthless if they can&amp;rsquo;t be restored. This is the strategy to restore the data from the remote location. This assumes the data in &lt;em>both&lt;/em> my local disks can&amp;rsquo;t be trusted anymore, so I&amp;rsquo;ll restore the backups from remote into a brand new disk with an empty zpool.&lt;/p>
&lt;p>While creating the monthly backups, I run &lt;code>zfs send&lt;/code> against the &lt;code>&amp;lt;fullbackup&amp;gt;&lt;/code>. This means that the restoration process needs 1) the full backup; 2) the latest &lt;em>monthly&lt;/em> backup and 3) all daily backups since the last monthly backup. This way &lt;em>most&lt;/em> of my data will be restored by obtaining just two files, while I still retain the daily granularity if my future self wishes to use it.&lt;/p>
&lt;p>The &lt;code>backup-download&lt;/code> command below is an unpublished tool that will download, decrypt and decompress the file from a remote location.&lt;/p>
&lt;p>Commands:&lt;/p>
&lt;pre tabindex="0">&lt;code>backup-download full/2020-08-24 | zfs recv data
backup-download monthly/2020-12-01 | zfs recv data
backup-download daily/2020-12-02 | zfs recv data
backup-download daily/2020-12-03 | zfs recv data
...
backup-download daily/2020-12-19 | zfs recv data
&lt;/code>&lt;/pre>&lt;h2 id="outro">Outro&lt;/h2>
&lt;p>This is just an experiment and this blog post is a live document that will be updated as I fine tune my strategy. I am yet to buy a new disk and attempt full restoration from remote (I did small scale tests only). I am not too worried about losing data at this point because I have another external disk, without ZFS, that I manually copy all the data into regularly. If you want to try this yourself be careful, as you can lose data.&lt;/p></content><id>https://www.kassner.com.br/en/2020/12/19/zfs-backup-strategy/</id><category term="linux"/></entry><entry><title>Fedora 28 on Lenovo Y720-15IBK</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2018/06/27/fedora-28-on-lenovo-y720-15ibk/"/><published>2018-06-27T00:00:00Z</published><updated>2018-06-27T00:00:00Z</updated><content type="html">&lt;p>It&amp;rsquo;s almost a year since I purchased this gaming computer and just now I had the need to do anything else than gaming in it. I have a friend that recently bought a notebook with a Nvidia GTX 1060 and he installed Fedora, making it a very snappy workstation, so I decided to give Fedora a go again. My computer came with Windows 10 by default and I never changed anything there, so here I am writing down all knowledge I got from installing Fedora in this machine.&lt;/p>
&lt;h2 id="partitioning">Partitioning&lt;/h2>
&lt;p>Partitioning the disk using the Windows disk management tool worked just fine.&lt;/p>
&lt;h2 id="live-usb">Live USB&lt;/h2>
&lt;p>Fedora Media Writer for Windows worked as intended.&lt;/p>
&lt;h2 id="rebooting-into-the-live-usb">Rebooting into the Live USB&lt;/h2>
&lt;p>Two tricky things. First one is &lt;a href="https://www.windowscentral.com/how-disable-windows-10-fast-startup">disable Fastboot&lt;/a>. The second one is that even disabling it on Windows, I still couldn&amp;rsquo;t get into my BIOS. I had to click on restart while holding shift and then use the Windows repair menu to get into the BIOS. Once inside the BIOS, I disabled the Fastboot and also allowed the boot over USB that was needed. Figuring out how to make into the BIOS took me some time also, but somehow I learned that I should use &lt;code>Fn + F2&lt;/code> for the BIOS and &lt;code>Fn + F12&lt;/code> for the boot menu.&lt;/p>
&lt;h2 id="disk-not-recognized">Disk not recognized&lt;/h2>
&lt;p>Booting into the Live USB was fine, but the installer could not recognize my NVMe disk at all. After a lot of research I landed into this tip that was to set the &lt;code>Sata Controller Mode&lt;/code> to &lt;code>AHCI&lt;/code>. I had &lt;code>Intel RST Premium&lt;/code> configured there and doing the change, Fedora was able to find my disk.&lt;/p>
&lt;p>Sadly, I could not get back to Windows with that configuration. Selecting Windows on GRUB will reboot the computer and start attempt to load Windows, but eventually I get an Windows-like error message saying that was not possible to boot. The error code was &lt;code>INACCESSIBLE_BOOT_DEVICE&lt;/code>.&lt;/p>
&lt;p>It took me a couple of hours investigating just this issue until I managed to make it work. I had to &lt;a href="http://support.thinkcritical.com/kb/articles/switch-windows-10-from-raid-ide-to-ahci">Switch Windows 10 from RAID/IDE to AHCI&lt;/a>, but I struggled a bit because I was doing that from the Repair tool instead of loading Windows normally. After I gave it a go from scratch, it worked as expected. Now I have dual boot on GRUB for Fedora and Windows, and my &lt;code>Sata Controller Mode&lt;/code> is set to &lt;code>AHCI&lt;/code>.&lt;/p>
&lt;h2 id="nvidia-driver">NVidia driver&lt;/h2>
&lt;h3 id="installation">Installation&lt;/h3>
&lt;p>&lt;a href="https://www.if-not-true-then-false.com/2015/fedora-nvidia-guide/">This tutorial&lt;/a> covers majority of the installation process, but I&amp;rsquo;ll detail here what went wrong.&lt;/p>
&lt;h3 id="issues">Issues&lt;/h3>
&lt;ul>
&lt;li>&lt;code>ERROR: could not insert 'nvidia_drm': Operation not permitted&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Disabling Secure Boot and SELinux while installing the driver fixed this problem. Providing the x509 cert and key created before to the NVidia installer did not help. I had to disable Secure Boot and sign it manually later.&lt;/p>
&lt;ul>
&lt;li>Wrong resolution&lt;/li>
&lt;/ul>
&lt;p>&lt;code>xrandr -q&lt;/code> was showing &lt;code>eDP-1-1 primary connected 960x540+0+0&lt;/code>. My monitor is 1080p. To fix that, I tried several different things, like setting the Mode manually inside the &lt;code>xorg.conf&lt;/code>, setting the DPI, extracting the EDID using NVidia tools, disable EDID usage, &lt;code>xrandr&lt;/code> with different combinations, and several variations inside the &lt;code>Xorg.conf&lt;/code>, but nothing worked. In the end, it was just a matter of enabling &lt;code>DPMS&lt;/code> and &lt;a href="https://gist.github.com/kassner/7efbbf6c5bb56ec1270df31983bad617">my xorg.conf&lt;/a> now looks like this.&lt;/p>
&lt;p>I also followed the &lt;a href="https://forum.manjaro.org/t/howto-set-up-prime-with-nvidia-proprietary-driver/40225">scripting part of this tutoria&lt;/a>, but I am not sure if this is doing anything for me right now. I also have the xrandr lines on my &lt;code>~/.profile&lt;/code>.&lt;/p>
&lt;h3 id="secure-boot">Secure Boot&lt;/h3>
&lt;p>I disabled Secure boot while installing the Nvidia driver, but later I enabled it back. It was less difficult than I expected. I followed &lt;a href="http://www.pellegrino.link/2015/11/29/signing-nvidia-proprietary-driver-on-fedora.html">this tutorial from Laurent&lt;/a>, but I am writing here it again for backup AND because I didn&amp;rsquo;t find the &lt;code>.ko&lt;/code> files in the first place (because they were compressed).&lt;/p>
&lt;p>First off, I had to generate some keys and add them to the Secure Boot mechanism, otherwise UEFI wouldn&amp;rsquo;t be able to know if the modules it is loading are trusted or not. For that, I needed a configuration file, which I created at &lt;code>~/x509.ini&lt;/code> with the content:&lt;/p>
&lt;pre tabindex="0">&lt;code>[req]
default_bits = 4096
distinguished_name = req_distinguised_name
prompt = no
string_mask = utf8only
x509_extensions = myexts
[req_distinguised_name]
O = kassner
CN = kassner
emailAddress = email@example.com
[myexts]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
&lt;/code>&lt;/pre>&lt;p>Then:&lt;/p>
&lt;ol>
&lt;li>&lt;code>openssl req -x509 -new -nodes -utf8 -sha256 -days 36500 -batch -config x509.ini -outform DER -out public_key.der -keyout private_key.priv&lt;/code>;&lt;/li>
&lt;li>&lt;code>mokutil --import public_key.der&lt;/code>;&lt;/li>
&lt;li>Reboot and follow the instructions to import the certificate;&lt;/li>
&lt;/ol>
&lt;p>And to sign the NVidia drivers:&lt;/p>
&lt;ol>
&lt;li>&lt;code>find / -name 'nvidia.ko.xz'&lt;/code>. The other files were in the same folder;&lt;/li>
&lt;li>Backup them somewhere else;&lt;/li>
&lt;li>&lt;code>xz --decompress *.ko.xz&lt;/code>;&lt;/li>
&lt;li>&lt;code>/usr/src/kernels/$(uname -r)/scripts/sign-file sha256 ~/private_key.priv ~/public_key.der nvidia.ko&lt;/code> and for the other &lt;code>.ko&lt;/code> files;&lt;/li>
&lt;li>&lt;code>xz --compress nvidia.ko&lt;/code> and the other &lt;code>.ko&lt;/code> files as well;&lt;/li>
&lt;li>&lt;code>modinfo nvidia&lt;/code> to be sure they are working;&lt;/li>
&lt;/ol>
&lt;h2 id="language-keyboard-locale-etc">Language, keyboard, locale, etc&lt;/h2>
&lt;p>I have a weird keyboard and language combination that I had to address for this computer. My computer has a Swedish keyboard, with the &lt;code>ä ö å&lt;/code> keys. I have an USB keyboard that shares the same layout. But, I was raised with a US keyboard and I do write Portuguese every day as well, so writing letters like &lt;code>é ç ã&lt;/code> is essential for me. At the same time, I rather use the US keyboard layout instead, since my muscle memory is already built around it. To orchestrate all this mess, I spent several days trying out different things until I finally managed.&lt;/p>
&lt;p>Sadly, since it was so scattered around and I couldn&amp;rsquo;t remember everything I did, I am most likely missing some things here. I am writing what I remember and I&amp;rsquo;ll update in the future if I ever find out what is wrong.&lt;/p>
&lt;h3 id="dates">Dates&lt;/h3>
&lt;p>&lt;code>date&lt;/code> and some other parts of some apps were returning localized information for me in Swedish. This was mainly due the fact that only &lt;code>$LANG&lt;/code> was set to &lt;code>en_US.UTF-8&lt;/code> and all other variables were set to &lt;code>sv_SE.UTF-8&lt;/code>, because I selected &lt;code>English&lt;/code> as language and &lt;code>Sweden (English)&lt;/code> as format.&lt;/p>
&lt;p>To fix this, I tried several different things, and what I believe it worked was:&lt;/p>
&lt;pre tabindex="0">&lt;code>sudo localedef -v -c -i en_US -f UTF-8 en_SE.UTF-8
sudo localectl set-locale LANG=en_SE.UTF-8
&lt;/code>&lt;/pre>&lt;p>And keep my &lt;code>/etc/locale.conf&lt;/code> like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>LANG=en_SE.UTF-8
LC_CTYPE=&amp;#34;en_SE.UTF-8&amp;#34;
LC_NUMERIC=&amp;#34;en_SE.UTF-8&amp;#34;
LC_TIME=&amp;#34;en_SE.UTF-8&amp;#34;
LC_COLLATE=&amp;#34;en_SE.UTF-8&amp;#34;
LC_MONETARY=&amp;#34;en_SE.UTF-8&amp;#34;
LC_MESSAGES=&amp;#34;en_SE.UTF-8&amp;#34;
LC_PAPER=&amp;#34;en_SE.UTF-8&amp;#34;
LC_NAME=&amp;#34;en_SE.UTF-8&amp;#34;
LC_ADDRESS=&amp;#34;en_SE.UTF-8&amp;#34;
LC_TELEPHONE=&amp;#34;en_SE.UTF-8&amp;#34;
LC_MEASUREMENT=&amp;#34;en_SE.UTF-8&amp;#34;
LC_IDENTIFICATION=&amp;#34;en_SE.UTF-8&amp;#34;
LC_ALL=
&lt;/code>&lt;/pre>&lt;h3 id="compose">Compose&lt;/h3>
&lt;p>I copied from &lt;code>/usr/share/X11/locale/en_US.UTF-8/Compose&lt;/code> to &lt;code>~/.XCompose&lt;/code> and applied the following diff.&lt;/p>
&lt;pre tabindex="0">&lt;code>--- Compose 2018-06-23 21:37:15.647432197 +0200
+++ .XCompose 2018-06-23 22:19:59.832729155 +0200
@@ -611,7 +611,7 @@
&amp;lt;Multi_key&amp;gt; &amp;lt;asterisk&amp;gt; &amp;lt;A&amp;gt; : &amp;#34;Å&amp;#34; Aring # LATIN CAPITAL LETTER A WITH RING ABOVE
&amp;lt;Multi_key&amp;gt; &amp;lt;A&amp;gt; &amp;lt;asterisk&amp;gt; : &amp;#34;Å&amp;#34; Aring # LATIN CAPITAL LETTER A WITH RING ABOVE
&amp;lt;Multi_key&amp;gt; &amp;lt;A&amp;gt; &amp;lt;A&amp;gt; : &amp;#34;Å&amp;#34; Aring # LATIN CAPITAL LETTER A WITH RING ABOVE
-&amp;lt;dead_cedilla&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ç&amp;#34; Ccedilla # LATIN CAPITAL LETTER C WITH CEDILLA
+&amp;lt;dead_acute&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ç&amp;#34; Ccedilla # LATIN CAPITAL LETTER C WITH CEDILLA
&amp;lt;Multi_key&amp;gt; &amp;lt;comma&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ç&amp;#34; Ccedilla # LATIN CAPITAL LETTER C WITH CEDILLA
&amp;lt;Multi_key&amp;gt; &amp;lt;C&amp;gt; &amp;lt;comma&amp;gt; : &amp;#34;Ç&amp;#34; Ccedilla # LATIN CAPITAL LETTER C WITH CEDILLA
&amp;lt;Multi_key&amp;gt; &amp;lt;cedilla&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ç&amp;#34; Ccedilla # LATIN CAPITAL LETTER C WITH CEDILLA
@@ -736,7 +736,7 @@
&amp;lt;Multi_key&amp;gt; &amp;lt;asterisk&amp;gt; &amp;lt;a&amp;gt; : &amp;#34;å&amp;#34; aring # LATIN SMALL LETTER A WITH RING ABOVE
&amp;lt;Multi_key&amp;gt; &amp;lt;a&amp;gt; &amp;lt;asterisk&amp;gt; : &amp;#34;å&amp;#34; aring # LATIN SMALL LETTER A WITH RING ABOVE
&amp;lt;Multi_key&amp;gt; &amp;lt;a&amp;gt; &amp;lt;a&amp;gt; : &amp;#34;å&amp;#34; aring # LATIN SMALL LETTER A WITH RING ABOVE
-&amp;lt;dead_cedilla&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ç&amp;#34; ccedilla # LATIN SMALL LETTER C WITH CEDILLA
+&amp;lt;dead_acute&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ç&amp;#34; ccedilla # LATIN SMALL LETTER C WITH CEDILLA
&amp;lt;Multi_key&amp;gt; &amp;lt;comma&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ç&amp;#34; ccedilla # LATIN SMALL LETTER C WITH CEDILLA
&amp;lt;Multi_key&amp;gt; &amp;lt;c&amp;gt; &amp;lt;comma&amp;gt; : &amp;#34;ç&amp;#34; ccedilla # LATIN SMALL LETTER C WITH CEDILLA
&amp;lt;Multi_key&amp;gt; &amp;lt;cedilla&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ç&amp;#34; ccedilla # LATIN SMALL LETTER C WITH CEDILLA
@@ -873,11 +873,9 @@
&amp;lt;Multi_key&amp;gt; &amp;lt;a&amp;gt; &amp;lt;semicolon&amp;gt; : &amp;#34;ą&amp;#34; U0105 # LATIN SMALL LETTER A WITH OGONEK
&amp;lt;Multi_key&amp;gt; &amp;lt;comma&amp;gt; &amp;lt;a&amp;gt; : &amp;#34;ą&amp;#34; U0105 # LATIN SMALL LETTER A WITH OGONEK
&amp;lt;Multi_key&amp;gt; &amp;lt;a&amp;gt; &amp;lt;comma&amp;gt; : &amp;#34;ą&amp;#34; U0105 # LATIN SMALL LETTER A WITH OGONEK
-&amp;lt;dead_acute&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ć&amp;#34; U0106 # LATIN CAPITAL LETTER C WITH ACUTE
&amp;lt;Multi_key&amp;gt; &amp;lt;acute&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ć&amp;#34; U0106 # LATIN CAPITAL LETTER C WITH ACUTE
&amp;lt;Multi_key&amp;gt; &amp;lt;apostrophe&amp;gt; &amp;lt;C&amp;gt; : &amp;#34;Ć&amp;#34; U0106 # LATIN CAPITAL LETTER C WITH ACUTE
&amp;lt;Multi_key&amp;gt; &amp;lt;C&amp;gt; &amp;lt;apostrophe&amp;gt; : &amp;#34;Ć&amp;#34; U0106 # LATIN CAPITAL LETTER C WITH ACUTE
-&amp;lt;dead_acute&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ć&amp;#34; U0107 # LATIN SMALL LETTER C WITH ACUTE
&amp;lt;Multi_key&amp;gt; &amp;lt;acute&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ć&amp;#34; U0107 # LATIN SMALL LETTER C WITH ACUTE
&amp;lt;Multi_key&amp;gt; &amp;lt;apostrophe&amp;gt; &amp;lt;c&amp;gt; : &amp;#34;ć&amp;#34; U0107 # LATIN SMALL LETTER C WITH ACUTE
&amp;lt;Multi_key&amp;gt; &amp;lt;c&amp;gt; &amp;lt;apostrophe&amp;gt; : &amp;#34;ć&amp;#34; U0107 # LATIN SMALL LETTER C WITH ACUTE
&lt;/code>&lt;/pre>&lt;p>Weirdly, my current &lt;code>/usr/share/X11/locale/en_US.UTF-8/Compose&lt;/code> has no difference to my .XCompose, so I am not sure which one was the modification that made the trick.&lt;/p>
&lt;h3 id="keyboard">Keyboard&lt;/h3>
&lt;p>The keyboard layout &lt;code>English (intl., with dead keys)&lt;/code> works as expected in (almost) all apps. &lt;code>~ + C&lt;/code> gives me &lt;code>ç&lt;/code> as &lt;code>' + e&lt;/code> gives me &lt;code>é&lt;/code>. Except PhpStorm. I can&amp;rsquo;t successfully type &lt;code>&amp;quot; ' ~ &lt;/code> or the backtick within the editor. Just nothing gets typed. The PhpStorm Keymap settings can recognize when I type one of those keys, but the editor itself or other menus wouldn&amp;rsquo;t. I tried several different things, related with IBus, changing &lt;code>XMODIFIERS&lt;/code>, &lt;code>/etc/environment&lt;/code>, &lt;code>~/.Xmodmap&lt;/code> and &lt;code>~/.profile&lt;/code>, saw some bug reports on JetBrains tracker, but nothing really worked.&lt;/p>
&lt;p>My current workaround is to have &lt;code>English (intl, with AltGr dead keys)&lt;/code> as a second keyboard layout that I use while I am on PhpStorm. Since I only write code (and comments) in English, it won&amp;rsquo;t be much of a problem, but I am having occasional mindfucks because I&amp;rsquo;ll keep forgetting to switch between keyboard layouts when I switch applications.&lt;/p></content><id>https://www.kassner.com.br/en/2018/06/27/fedora-28-on-lenovo-y720-15ibk/</id><category term="linux"/></entry><entry><title>SOCKS proxy with SSH</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2018/06/25/socks-proxy-with-ssh/"/><published>2018-06-25T00:00:00Z</published><updated>2018-06-25T00:00:00Z</updated><content type="html">&lt;p>That&amp;rsquo;s not a new thing, but I happened to use it during the weekend to be able to access some services back in Brazil
that were IP-limited and HideMyAss couldn&amp;rsquo;t help, so I asked a friend for a small proxy help.&lt;/p>
&lt;p>What I did on my side:&lt;/p>
&lt;ol>
&lt;li>Opened a port on my modem to forward the connection to the port 51000 on my computer;&lt;/li>
&lt;li>Started a container: &lt;code>docker run -p 51000:51000 -p 51001:51001 --rm -it ubuntu:xenial bash&lt;/code>;&lt;/li>
&lt;li>Installed &lt;code>supervisord&lt;/code>, &lt;code>openssh-server&lt;/code> and added &lt;code>GatewayPorts yes&lt;/code> to &lt;code>/etc/ssh/sshd_config&lt;/code>;&lt;/li>
&lt;/ol>
&lt;p>My friend had to run those commands in parallel:&lt;/p>
&lt;ol>
&lt;li>&lt;code>ssh -D 51001 localhost&lt;/code>;&lt;/li>
&lt;li>&lt;code>ssh -R :51001:localhost:51001 use@myIP -p 51000&lt;/code>;&lt;/li>
&lt;/ol>
&lt;p>And voilá, I had a SOCKS proxy over &lt;code>localhost:51000&lt;/code> that was going out to the internet using a Brazilian IP address.&lt;/p></content><id>https://www.kassner.com.br/en/2018/06/25/socks-proxy-with-ssh/</id><category term="linux"/><category term="network"/></entry><entry><title>Profiling browser requests with Blackfire</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2017/06/16/profiling-browser-requests-with_blackfire/"/><published>2017-06-16T00:00:00Z</published><updated>2017-06-16T00:00:00Z</updated><content type="html">&lt;p>This week&amp;rsquo;s task was optimising Magento for large carts (100+ different products) and a profiler is a good tool to find small pieces of code that could be optimised. &lt;a href="https://www.blackfire.io/">Blackfire&lt;/a> was my choice, mostly because previous experience, but also because it is not a resource hog like Xdebug, which was taking around 6 minutes while Blackfire took only 20 seconds in the same type of request.&lt;/p>
&lt;h2 id="companion">Companion&lt;/h2>
&lt;p>Blackfire works great to profile GET requests on the browser with the help of &lt;a href="https://blackfire.io/docs/integrations/chrome">Companion&lt;/a>, but it doesn&amp;rsquo;t allow you to profile POST requests. Also, Companion sends multiple requests to the same endpoint, but Magento&amp;rsquo;s place order is not stateless, so the first and all other request would differ substantially.&lt;/p>
&lt;h2 id="cli-tool">CLI tool&lt;/h2>
&lt;p>Second attempt was the Blackfire &lt;a href="https://blackfire.io/docs/cookbooks/profiling-http">CLI tool&lt;/a>, which allows you to profile anything you can reach with &lt;code>curl&lt;/code>, including POST requests and custom headers. But, you can only profile one request at the given time, so that means I need to assemble one HTTP call that does everything: login customer, add products to cart, collect totals, get shipment information, get payment information, save shipping, save payment, and finally place order. That way I am profiling a lot more than I need, not to mention that everything will be in the same requests and not have the same performance as the end customer will experience, since a lot of objects will have been cached in memory when the place order part is executed. So I ruled that out too.&lt;/p>
&lt;h2 id="xdebug">Xdebug&lt;/h2>
&lt;p>Xdebug does exactly what I want, but takes too much time, being almost impractical. You can enable the profiling for any HTTP request, so that wouldn&amp;rsquo;t be a problem, I just use my browser and to navigate through all the needed steps (I can even do that with Xdebug disabled and enable only for the last step) and then analyse the cachegrind files. But Xdebug uses too much resources, I was hitting a 5-minute timeout on FPM too easily and the results were contaminated by the slowness (everything was slower in a factor of ~5 compared to the latest Blackfire result).&lt;/p>
&lt;p>One nice tip here: you can use &lt;code>blackfire upload&lt;/code> to send the Xdebug output files to Blackfire and see the analysis there, I am pretty amazed by that feature. Way to go SensioLabs!&lt;/p>
&lt;h2 id="custom-headers">Custom headers&lt;/h2>
&lt;p>With a bit of back and forth with SensioLabs support I was able to achieve what I wanted, even if requires a bit more work.&lt;/p>
&lt;h3 id="requirements">Requirements&lt;/h3>
&lt;ol>
&lt;li>Extension for your browser to modify the request headers (I used &lt;a href="https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=en">ModHeader&lt;/a> for Google Chrome);&lt;/li>
&lt;li>Blackfire CLI tool;&lt;/li>
&lt;li>Blackfire configured on the webserver;&lt;/li>
&lt;/ol>
&lt;h3 id="steps">Steps&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>Do all the preparation needed. I added all products to the cart, navigated to checkout page and filled all the information, only leaving out the &amp;lsquo;click on place order button&amp;rsquo; step;&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Run &lt;code>blackfire run env&lt;/code> and copy the value of the &lt;code>BLACKFIRE_QUERY&lt;/code> outputted variable;&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Using the browser extension, add the following headers to your request (&lt;a href="http://i.imgur.com/QMjadko.png">example&lt;/a>):&lt;/p>
&lt;pre tabindex="0">&lt;code>X-Blackfire-User-Agent: Blackfire Companion - Chrome/58.0.3029.110 Extension/1.10.0
X-Blackfire-Query: [the value copied on #2]
&lt;/code>&lt;/pre>&lt;/li>
&lt;li>
&lt;p>Execute your request (click the place order button).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="http://i.imgur.com/kIU3epD.png">Profit&lt;/a>.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>With those steps, the first AJAX/POST request (that reaches PHP) with those headers will be profiled. &lt;em>It only works once&lt;/em>, so it will not profile the success page (which is the one the request I profiled redirects to). Every new request needs a new &lt;code>X-Blackfire-Query&lt;/code> header, so with this approach is not (yet) possible to profile the redirected page as well or other subsequent calls inside the same page.&lt;/p>
&lt;p>If the request you want to profile is not the first one that you are able to fire, instead of using the extension on the browser, change your Javascript code to include those headers only for the request you need (or keep the extension but change the Javascript to remove them).&lt;/p>
&lt;h2 id="what-i-have-not-tried">What I have not tried&lt;/h2>
&lt;p>There are two other ways that I could profile what I wanted with Blackfire, but I haven&amp;rsquo;t tested:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Use the &lt;a href="https://blackfire.io/docs/reference-guide/php-sdk">PHP SDK&lt;/a> and start/stop the profiling on the areas that you need. It sounds like is something that will work, but that means you need to do some changes on code. I would only go that route if I hadn&amp;rsquo;t found any alternative.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Using the scenario I described on the &lt;a href="/en/2017/06/16/profiling-browser-requests-with_blackfire/#cli-tool">CLI tool&lt;/a>, but using the technic presented on &lt;a href="https://blog.blackfire.io/profiling-http-sub-requests-using-blackfire.html">Profiling HTTP Sub-Requests using Blackfire&lt;/a>. To achieve I would had to assemble all the HTTP calls as the same way as Magento would have done, and send the &lt;code>X-Blackfire-Query&lt;/code> header with them. This will profile all the calls individually (thus not having the &lt;em>in-memory cache&lt;/em> problem), but it also requires a bit of coding, or preparing all the HTTP requests manually to some degree.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Blackfire is a nice tool and I really like the help their support provided here. I wish they cover this edge case in their roadmap, but after this &lt;em>discovery&lt;/em> I am definitely not turning it down, I am very happy with the results. Please consider giving Blackfire a try if you need it.&lt;/p></content><id>https://www.kassner.com.br/en/2017/06/16/profiling-browser-requests-with_blackfire/</id><category term="php"/><category term="magento"/><category term="testing"/></entry><entry><title>netcat replacement Ncat time unit</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2014/07/21/netcat-replacement-ncat-time-unit/"/><published>2014-07-21T00:00:00Z</published><updated>2014-07-21T00:00:00Z</updated><content type="html">&lt;p>Hi,&lt;/p>
&lt;p>It&amp;rsquo;s common to use &lt;code>netcat&lt;/code> utility to work with SSH ProxyCommand, which allows to use a bridge server, very useful when you need to connect directly to a host behind a firewall. Example:&lt;/p>
&lt;pre tabindex="0">&lt;code># File: ~/.ssh/config
Host workbox
HostName 192.168.1.92 # The ip address that the bridge server can see
User anotherusername
IdentityFile ~/.ssh/id_rsa
ProxyCommand ssh myusername@firewall.company.com nc -w 120 %h %p
&lt;/code>&lt;/pre>&lt;p>I was using &lt;code>netcat&lt;/code> until a couple of days, and works very smooth. Then I&amp;rsquo;ve setup a new server on another private network, behind a CentOS 7 firewall, and when I&amp;rsquo;ve tried the configuration above, I got this:&lt;/p>
&lt;pre tabindex="0">&lt;code>Ncat: Since April 2010, the default unit for -w is seconds, so your time of &amp;#34;120&amp;#34; is 2.0 minutes. Use &amp;#34;120ms&amp;#34; for 120 milliseconds. QUITTING.
&lt;/code>&lt;/pre>&lt;p>I didn&amp;rsquo;t know, but I&amp;rsquo;m using &lt;a href="http://nmap.org/ncat/">Ncat&lt;/a> instead of the old &lt;a href="http://netcat.sourceforge.net/">Netcat&lt;/a>. I got not time to learn the differences, but I already found one. Every parameter that receives a time needs the unit. From &lt;code>ncat --help&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ nc --help
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ncat 6.40 &lt;span style="color:#f92672">(&lt;/span> http://nmap.org/ncat &lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Usage: ncat &lt;span style="color:#f92672">[&lt;/span>options&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#f92672">[&lt;/span>hostname&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#f92672">[&lt;/span>port&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Options taking a time assume seconds. Append &lt;span style="color:#e6db74">&amp;#39;ms&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> milliseconds,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#39;s&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> seconds, &lt;span style="color:#e6db74">&amp;#39;m&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> minutes, or &lt;span style="color:#e6db74">&amp;#39;h&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> hours &lt;span style="color:#f92672">(&lt;/span>e.g. 500ms&lt;span style="color:#f92672">)&lt;/span>.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, in my SSH configuration I had to change the &lt;code>-w&lt;/code> param to add the time unit (&lt;code>ms&lt;/code>, &lt;code>s&lt;/code>, &lt;code>m&lt;/code>, &lt;code>h&lt;/code>, etc):&lt;/p>
&lt;pre tabindex="0">&lt;code> ProxyCommand ssh myusername@firewall.company.com nc -w 120m %h %p
&lt;/code>&lt;/pre>&lt;p>Bye.&lt;/p></content><id>https://www.kassner.com.br/en/2014/07/21/netcat-replacement-ncat-time-unit/</id><category term="linux"/><category term="network"/></entry><entry><title>Magento: frontend and admin routes conflict</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2014/06/06/magento-frontend-and-admin-routes-conflict/"/><published>2014-06-06T00:00:00Z</published><updated>2014-06-06T00:00:00Z</updated><content type="html">&lt;p>Today I was working in a 3rd party module. Something very simple, an AJAX Add to Cart button. But, when I requested &lt;code>http://.../ajaxcart/cart/add&lt;/code>, I&amp;rsquo;ve got a 302 HTTP status, or, a redirect, to the same requested URL, but in secure mode (HTTPS).&lt;/p>
&lt;p>After debugging, I could understand that the controller wasn&amp;rsquo;t being called, so I try to find a configuration problem. The Administration Panel was configured to be served on HTTPS (Configuration &amp;gt; General &amp;gt; Web &amp;gt; Secure &amp;gt; ???), and when I&amp;rsquo;ve removed this option, everything was working as expected.&lt;/p>
&lt;p>Long story short, here is the problem:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;frontend&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;routers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;ajaxcart&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;use&amp;gt;&lt;/span>standard&lt;span style="color:#f92672">&amp;lt;/use&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;args&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;module&amp;gt;&lt;/span>Company_AjaxCart&lt;span style="color:#f92672">&amp;lt;/module&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;frontname&amp;gt;&lt;/span>ajaxcart&lt;span style="color:#f92672">&amp;lt;/frontname&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/args&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/routename&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/routers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/frontend&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;admin&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;routers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;ajaxcart&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;use&amp;gt;&lt;/span>admin&lt;span style="color:#f92672">&amp;lt;/use&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;args&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;module&amp;gt;&lt;/span>Company_AjaxCart&lt;span style="color:#f92672">&amp;lt;/module&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;frontname&amp;gt;&lt;/span>ajaxcart&lt;span style="color:#f92672">&amp;lt;/frontname&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/args&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/routename&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/routers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/admin&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Did you see that &lt;code>frontname&lt;/code> is the same both for frontend and admin? In this way, every time we call a URL with the pattern &lt;code>http://.../ajaxcart/*&lt;/code>, Magento replies you to redirect to secure mode. When using AJAX, we have to main problems with this:&lt;/p>
&lt;ol>
&lt;li>Developers don&amp;rsquo;t often worry about HTTP Status codes that aren&amp;rsquo;t 200;&lt;/li>
&lt;li>You have to configure the &lt;code>Same-Origin Policy&lt;/code>, or you&amp;rsquo;ll get &lt;code>CORS&lt;/code> errors like this:&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://.../ajaxcart/cart/add/. This can be fixed by moving the resource to the same domain or enabling CORS.
&lt;/code>&lt;/pre>&lt;p>TL;DR: On every module you develop for Magento which needs controllers in admin and frontend, pay attention and don&amp;rsquo;t use the same &lt;code>frontname&lt;/code>.&lt;/p>
&lt;p>Bye.&lt;/p></content><id>https://www.kassner.com.br/en/2014/06/06/magento-frontend-and-admin-routes-conflict/</id><category term="magento"/><category term="php"/><category term="redirect"/><category term="http"/></entry><entry><title>Nginx: regex on server_name</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2014/05/23/nginx-regex-on-server-name/"/><published>2014-05-23T00:00:00Z</published><updated>2014-05-23T00:00:00Z</updated><content type="html">&lt;p>Quick tip to use regex on nginx&amp;rsquo;s &lt;a href="http://nginx.org/en/docs/http/server_names.html">server_name&lt;/a> directive.&lt;/p>
&lt;p>Using a specific root directory for every subdomain:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>server &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> listen 80;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server_name ~^&lt;span style="color:#f92672">(&lt;/span>.*&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#ae81ff">\.&lt;/span>project&lt;span style="color:#ae81ff">\.&lt;/span>com$;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root /home/www/project/$1;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Using an environment variable with a single project:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>server &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> listen 80;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server_name ~^&lt;span style="color:#f92672">(&lt;/span>.*&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#ae81ff">\.&lt;/span>project&lt;span style="color:#ae81ff">\.&lt;/span>com$;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fastcgi_param CUSTOMER $1;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root /home/www/project;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can also use the entire domain name with this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>server &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> listen 80;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server_name ~^&lt;span style="color:#f92672">(&lt;/span>.*&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#ae81ff">\.&lt;/span>&lt;span style="color:#f92672">(&lt;/span>.*&lt;span style="color:#ae81ff">\.&lt;/span>.*&lt;span style="color:#f92672">)&lt;/span>$;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root /home/www/$2/subdomains/$1/public_html;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tested with Nginx 1.4.x.&lt;/p></content><id>https://www.kassner.com.br/en/2014/05/23/nginx-regex-on-server-name/</id><category term="nginx"/></entry><entry><title>Debian: cannot connect to X server</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2014/02/07/debian-cannot-connect-to-x-server/"/><published>2014-02-07T00:00:00Z</published><updated>2014-02-07T00:00:00Z</updated><content type="html">&lt;p>Quick tip on KDE based Debian:&lt;/p>
&lt;p>When you receive the following error when trying to run a X application with sudo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kassner@brian:~$ sudo unetbootin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>No protocol specified
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unetbootin: cannot connect to X server :0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Run the following command and try again:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kassner@brian:~$ xhost SI:localuser:root
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>localuser:root being added to access control list
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content><id>https://www.kassner.com.br/en/2014/02/07/debian-cannot-connect-to-x-server/</id><category term="debian"/><category term="kde"/><category term="linux"/><category term="super user"/></entry><entry><title>Twig_Error_Syntax: A message inside a trans tag must be a simple text</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2014/01/15/twig_error_syntax-a-message-inside-a-trans-tag-must-be-a-simple-text/"/><published>2014-01-15T00:00:00Z</published><updated>2014-01-15T00:00:00Z</updated><content type="html">&lt;p>Quick tip:&lt;/p>
&lt;p>When have a code like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>{&lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#a6e22e">raw&lt;/span> &lt;span style="color:#f92672">%&lt;/span>}{&lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#a6e22e">trans&lt;/span> &lt;span style="color:#f92672">%&lt;/span>}&lt;span style="color:#a6e22e">prefix&lt;/span>&lt;span style="color:#f92672">.&lt;/span>{{ &lt;span style="color:#a6e22e">varname&lt;/span> }}{&lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#a6e22e">endtrans&lt;/span> &lt;span style="color:#f92672">%&lt;/span>}{&lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#a6e22e">endraw&lt;/span> &lt;span style="color:#f92672">%&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And receive this error:&lt;/p>
&lt;pre tabindex="0">&lt;code>Twig_Error_Syntax: A message inside a trans tag must be a simple text
&lt;/code>&lt;/pre>&lt;p>You can use the following piece of code as workaround.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>{&lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#a6e22e">raw&lt;/span> &lt;span style="color:#f92672">%&lt;/span>}{{ (&lt;span style="color:#e6db74">&amp;#34;prefix.&amp;#34;&lt;/span> &lt;span style="color:#f92672">~&lt;/span> &lt;span style="color:#a6e22e">varname&lt;/span>)&lt;span style="color:#f92672">|&lt;/span>&lt;span style="color:#a6e22e">trans&lt;/span> }}{&lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#a6e22e">endraw&lt;/span> &lt;span style="color:#f92672">%&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>PS: maybe this is not the best way to go.&lt;/p></content><id>https://www.kassner.com.br/en/2014/01/15/twig_error_syntax-a-message-inside-a-trans-tag-must-be-a-simple-text/</id><category term="php"/><category term="symfony2"/><category term="twig"/></entry><entry><title>Symfony 2: Inheritance and UniqueEntity workaround</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2013/11/27/symfony-2-inheritance-and-uniqueentity-workaround/"/><published>2013-11-27T00:00:00Z</published><updated>2013-11-27T00:00:00Z</updated><content type="html">&lt;p>Hi folks,&lt;/p>
&lt;p>Today I struggled over an already &lt;a href="https://github.com/symfony/symfony/issues/4087">known bug on Symfony2&lt;/a> when using UniqueEntity and Entity Inheritance. In the bug discussion, &lt;a href="https://github.com/symfony/symfony/issues/4087#issuecomment-25349274">@gentisaliu recommended&lt;/a> using a custom repository, and how do this I’m documenting here.&lt;/p>
&lt;p>Entities:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\Table(name=&amp;#34;parent&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\Entity(repositoryClass=&amp;#34;Repository\Parent&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @UniqueEntity(fields={&amp;#34;name&amp;#34;}, repositoryMethod=&amp;#34;findByName&amp;#34;, message=&amp;#34;Name already used.&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\InheritanceType(&amp;#34;JOINED&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\DiscriminatorColumn(name=&amp;#34;type&amp;#34;, type=&amp;#34;string&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\DiscriminatorMap({
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * &amp;#34;a&amp;#34; = &amp;#34;ChildA&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * &amp;#34;b&amp;#34; = &amp;#34;ChildB&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * })
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Parent&lt;/span> { }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\Entity(repositoryClass=&amp;#34;Repository\Parent&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\Table(name=&amp;#34;child_a&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">ChildA&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#66d9ef">Parent&lt;/span> { }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\Entity(repositoryClass=&amp;#34;Repository\Parent&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * @ORM\Table(name=&amp;#34;child_b&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">ChildB&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#66d9ef">Parent&lt;/span> { }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>repositoryMethod:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">findByName&lt;/span>($criteria)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> $this
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">getEntityManager&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">createQuery&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;SELECT e FROM Parent e WHERE e.name = :name&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">setParameters&lt;/span>($criteria)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">getResult&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>The findByName method must explicitly query the main entity, otherwise you will check a the uniqueness only for that type (name = ? AND type = ?)&lt;/li>
&lt;li>The repositoryMethod defines which method in the repository will be used for determine if an entity is unique or not. You have to return an Countable object, array allowed, but I rather use a query result.&lt;/li>
&lt;li>The repositoryClass must be the same of all entities, or if you can’t use the same repository, implement your “repositoryMethod” in all repositories. In such case you have to be cautious to do the query the base entity, not the child one.&lt;/li>
&lt;/ol>
&lt;p>That’s all.&lt;/p></content><id>https://www.kassner.com.br/en/2013/11/27/symfony-2-inheritance-and-uniqueentity-workaround/</id><category term="php"/><category term="symfony2"/><category term="doctrine"/></entry><entry><title>Debian: ping unknown host but DNS works</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2013/11/14/debian-ping-unknown-host-but-dns-works/"/><published>2013-11-14T00:00:00Z</published><updated>2013-11-14T00:00:00Z</updated><content type="html">&lt;p>Hello.&lt;/p>
&lt;p>Today I bumped into a problem with ping:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kassner@brian$ ping git.company.local
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ping: unknown host git.company.local
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Of course, it’s a local address, so maybe I forgot to add the local DNS server. Let’s check:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kassner@brian$ dig A git.company.local
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>; &amp;lt; &amp;lt;&amp;gt;&amp;gt; DiG 9.8.4-rpz2+rl005.12-P1 &amp;lt; &amp;lt;&amp;gt;&amp;gt; A git.company.local
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; global options: +cmd
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; Got answer:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; -&amp;gt;&amp;gt;HEADER&amp;lt; &amp;lt;- opcode: QUERY, status: NOERROR, id: &lt;span style="color:#ae81ff">15746&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: &lt;span style="color:#ae81ff">4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; QUESTION SECTION:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;git.company.local. IN A
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; ANSWER SECTION:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git.company.local. &lt;span style="color:#ae81ff">86400&lt;/span> IN A 192.168.0.150
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; Query time: &lt;span style="color:#ae81ff">0&lt;/span> msec
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; SERVER: 10.0.0.1#53&lt;span style="color:#f92672">(&lt;/span>10.0.0.1&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; WHEN: Thu Nov &lt;span style="color:#ae81ff">14&lt;/span> 12:05:45 &lt;span style="color:#ae81ff">2013&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>;; MSG SIZE rcvd: &lt;span style="color:#ae81ff">249&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>WTF? Oh, of course. I’m using a &lt;code>.local&lt;/code> suffix, so &lt;a href="http://avahi.org/">Avahi&lt;/a> will take action. Needless for my local network, I just disabled it on Debian Wheezy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kassner@brian$ sudo update-rc.d -f avahi-daemon remove
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This should disable Avahi temporarely (until reboot). But, if you need to get rid of Avahi, just uninstall:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kassner@brian$ sudo apt-get remove avahi-daemon
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Bye.&lt;/p></content><id>https://www.kassner.com.br/en/2013/11/14/debian-ping-unknown-host-but-dns-works/</id><category term="avahi"/><category term="dns"/><category term="icmp"/><category term="linux"/><category term="network"/><category term="ping"/></entry><entry><title>Magento Product object and loadByAttribute</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2012/05/28/magento-product-object-and-loadbyattribute/"/><published>2012-05-28T00:00:00Z</published><updated>2012-05-28T00:00:00Z</updated><content type="html">&lt;p>Hi Folks,&lt;/p>
&lt;p>A little note to work with product object in Magento:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * Using setStoreId two times, otherwise it will save the data to default store
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$product &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">Mage&lt;/span>&lt;span style="color:#f92672">::&lt;/span>&lt;span style="color:#a6e22e">getModel&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;catalog/product&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">setStoreId&lt;/span>($storeId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">loadByAttribute&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;ean&amp;#39;&lt;/span>, $ean)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span>&lt;span style="color:#a6e22e">setStoreId&lt;/span>($storeId);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content><id>https://www.kassner.com.br/en/2012/05/28/magento-product-object-and-loadbyattribute/</id><category term="php"/><category term="magento"/></entry><entry><title>Apache Rewrite on VirtualHost = 400 Bad Request Error</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2012/03/07/apache-rewrite-on-virtualhost-400-bad-request-error/"/><published>2012-03-07T00:00:00Z</published><updated>2012-03-07T00:00:00Z</updated><content type="html">&lt;p>Mental note:&lt;/p>
&lt;p>If you are moving the Rewrite from &lt;code>.htaccess&lt;/code> to VirtualHost configuration and get a 400 Bad Request error, one or both tips below can be useful:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>RewriteEngine On
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Use the %{DOCUMENT_ROOT}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RewriteCond %&lt;span style="color:#f92672">{&lt;/span>DOCUMENT_ROOT&lt;span style="color:#f92672">}&lt;/span>%&lt;span style="color:#f92672">{&lt;/span>REQUEST_FILENAME&lt;span style="color:#f92672">}&lt;/span> -s &lt;span style="color:#f92672">[&lt;/span>OR&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RewriteCond %&lt;span style="color:#f92672">{&lt;/span>DOCUMENT_ROOT&lt;span style="color:#f92672">}&lt;/span>%&lt;span style="color:#f92672">{&lt;/span>REQUEST_FILENAME&lt;span style="color:#f92672">}&lt;/span> -l &lt;span style="color:#f92672">[&lt;/span>OR&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RewriteCond %&lt;span style="color:#f92672">{&lt;/span>DOCUMENT_ROOT&lt;span style="color:#f92672">}&lt;/span>%&lt;span style="color:#f92672">{&lt;/span>REQUEST_FILENAME&lt;span style="color:#f92672">}&lt;/span> -d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RewriteRule ^.*$ - &lt;span style="color:#f92672">[&lt;/span>NC,L&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Use the absolute path&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RewriteRule ^.*$ /home/www/html/index.php &lt;span style="color:#f92672">[&lt;/span>NC,L&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That’s all.&lt;/p></content><id>https://www.kassner.com.br/en/2012/03/07/apache-rewrite-on-virtualhost-400-bad-request-error/</id><category term="apache"/></entry><entry><title>Converting movies with tovid</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2012/02/09/converting-movies-with-tovid/"/><published>2012-02-09T00:00:00Z</published><updated>2012-02-09T00:00:00Z</updated><content type="html">&lt;p>Hello&lt;/p>
&lt;p>A little tip to convert AVI files to DVD with embed subtitles.&lt;/p>
&lt;p>Installing tovid:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt-get install tovid
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Converting AVI file to a DVD-ISO file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>tovid -dvd -in Video.avi -subtitles Legenda.srt -out Video
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>/usr/share/tovid/makexml Video.mpg -out Video
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>export VIDEO_FORMAT&lt;span style="color:#f92672">=&lt;/span>NTSC
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>/usr/share/tovid/makedvd Video.xml
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mkisofs -dvd-video -udf -R -o Video.iso Video/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>So, you just need to burn your ISO file. You can use &lt;a href="http://projects.gnome.org/brasero/">Brasero&lt;/a>.&lt;/p></content><id>https://www.kassner.com.br/en/2012/02/09/converting-movies-with-tovid/</id><category term="linux"/><category term="video"/></entry><entry><title>Magento slow</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2011/01/07/magento-slow/"/><published>2011-01-07T00:00:00Z</published><updated>2011-01-07T00:00:00Z</updated><content type="html">&lt;p>Hello.&lt;/p>
&lt;p>After an intense debug into Magento, we (&lt;a href="http://www.ibaldo.com.br/">Filipe Ibaldo&lt;/a> and me) have found two problems in Magento code, related to slow Place Order with many products in cart (not many qty of a product).&lt;/p>
&lt;p>One of the problems is the order item save, which is executed precisely in &lt;code>app/code/core/Mage/Sales/Model/Entity/Order/Attribute/Backend/Parent.php&lt;/code>, on afterSave method, who spent many time on my machine (Core 2 Duo 2.26/4GB RAM), about 0.3 second for each product.&lt;/p>
&lt;p>Considering the number of items average of customer’s cart is about 50 distinct products, this time, adding to another code executions and a high server load, is easily more than one minute, and this is unacceptable.&lt;/p>
&lt;p>The found workaround is disable some observers from &lt;code>Mage_Downloadable&lt;/code> and &lt;code>Mage_Rss&lt;/code>, who execute in afterSave of each item in order.&lt;/p>
&lt;p>In &lt;code>app/code/core/Mage/Downloadable/etc/config.xml&lt;/code>, remove/comment the following lines:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;sales_order_item_save_commit_after&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;observers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;downloadable_observer&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;class&amp;gt;&lt;/span>downloadable/observer&lt;span style="color:#f92672">&amp;lt;/class&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;method&amp;gt;&lt;/span>saveDownloadableOrderItem&lt;span style="color:#f92672">&amp;lt;/method&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/downloadable_observer&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/observers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/sales_order_item_save_commit_after&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The code above appears twice.&lt;/p>
&lt;p>In &lt;code>app/code/core/Mage/Rss/etc/config.xml&lt;/code>, remove/comment the following lines:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;sales_order_save_after&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;observers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;notifystock&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;class&amp;gt;&lt;/span>rss/observer&lt;span style="color:#f92672">&amp;lt;/class&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;method&amp;gt;&lt;/span>salesOrderItemSaveAfterNotifyStock&lt;span style="color:#f92672">&amp;lt;/method&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/notifystock&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/observers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/sales_order_save_after&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;sales_order_save_after&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;observers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;ordernew&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;class&amp;gt;&lt;/span>rss/observer&lt;span style="color:#f92672">&amp;lt;/class&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;method&amp;gt;&lt;/span>salesOrderItemSaveAfterOrderNew&lt;span style="color:#f92672">&amp;lt;/method&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/ordernew&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;/observers&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;lt;/sales_order_save_after&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With this workaround, the save time of each product went from 0.3 second to 0.001 second, an incredible speed improvement.&lt;/p>
&lt;p>However, if you are using downloadable products or Magento Inventory Management, this modifications can broke related features, so, be careful.&lt;/p>
&lt;p>Another problem found is related with module disable feature into System &amp;gt; Configuration &amp;gt; Advanced &amp;gt; Advanced &amp;gt; Disable Module Output. If you disable some module, their observers did not disabled, and you need to remove their configuration or delete module folder.&lt;/p>
&lt;p>Bye.&lt;/p></content><id>https://www.kassner.com.br/en/2011/01/07/magento-slow/</id><category term="php"/><category term="magento"/><category term="performance"/></entry><entry><title>Include error on Zend_Loader</title><link rel="alternate" type="text/html" href="https://www.kassner.com.br/en/2010/11/01/include-error-on-zend-loader/"/><published>2010-11-01T00:00:00Z</published><updated>2010-11-01T00:00:00Z</updated><content type="html">&lt;p>Hello.&lt;/p>
&lt;p>Today I found a problem in Zend Framework and his class autoloader, where an &lt;a href="http://php.net/class_exists">class_exists&lt;/a> function call fires the Zend Framework autoloader. The function has an option to ignore the autoloader, but any class that I need loaded by Zend_Loader will return false.&lt;/p>
&lt;p>Then, if I call class_exists without disable autoloader, I got an file not exists PHP warning, that’s why Zend_Loader doesn’t check if the file exists, just include.&lt;/p>
&lt;p>The trick is change &lt;code>Zend/Loader.php&lt;/code> to check if file exists before include, and this must be wrote in loadFile method.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">loadFile&lt;/span>($filename, $dirs &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>, $once &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">self&lt;/span>&lt;span style="color:#f92672">::&lt;/span>&lt;span style="color:#a6e22e">_securityCheck&lt;/span>($filename);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * Search in provided directories, as well as include_path
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $incPath &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#f92672">!&lt;/span>&lt;span style="color:#66d9ef">empty&lt;/span>($dirs) &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> (&lt;span style="color:#a6e22e">is_array&lt;/span>($dirs) &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#a6e22e">is_string&lt;/span>($dirs))) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">is_array&lt;/span>($dirs)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $dirs &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">implode&lt;/span>(&lt;span style="color:#a6e22e">PATH_SEPARATOR&lt;/span>, $dirs);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $incPath &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_include_path&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">set_include_path&lt;/span>($dirs &lt;span style="color:#f92672">.&lt;/span> &lt;span style="color:#a6e22e">PATH_SEPARATOR&lt;/span> &lt;span style="color:#f92672">.&lt;/span> $incPath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * Try finding for the plain filename in the include_path.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#f92672">!&lt;/span>&lt;span style="color:#a6e22e">self&lt;/span>&lt;span style="color:#f92672">::&lt;/span>&lt;span style="color:#a6e22e">fileExists&lt;/span>($filename)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ($once) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">include_once&lt;/span> $filename;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">include&lt;/span> $filename;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> * If searching in directories, reset include_path
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ($incPath) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">set_include_path&lt;/span>($incPath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">fileExists&lt;/span> ($filename) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $paths &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">explode&lt;/span>(&lt;span style="color:#a6e22e">PATH_SEPARATOR&lt;/span>, &lt;span style="color:#a6e22e">get_include_path&lt;/span>());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">foreach&lt;/span> ($paths &lt;span style="color:#66d9ef">as&lt;/span> $path) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">file_exists&lt;/span>($path &lt;span style="color:#f92672">.&lt;/span> &lt;span style="color:#a6e22e">DIRECTORY_SEPARATOR&lt;/span> &lt;span style="color:#f92672">.&lt;/span> $filename)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Bye.&lt;/p></content><id>https://www.kassner.com.br/en/2010/11/01/include-error-on-zend-loader/</id><category term="php"/><category term="zend framework"/></entry></feed>