<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Waste of Server]]></title><description><![CDATA[where idle loops hang out]]></description><link>https://wasteofserver.com/</link><image><url>https://wasteofserver.com/favicon.png</url><title>Waste of Server</title><link>https://wasteofserver.com/</link></image><generator>Ghost 5.81</generator><lastBuildDate>Thu, 23 Apr 2026 11:07:57 GMT</lastBuildDate><atom:link href="https://wasteofserver.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[TP-Link HS100 - Godsend for Smart Home Integration]]></title><description><![CDATA[The following code works on the HS100, HS105 and HS110, it may be compatible with more models, but was tested with success on these.]]></description><link>https://wasteofserver.com/tplink-hs110/</link><guid isPermaLink="false">6931c715136c3f00014e577b</guid><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Thu, 04 Dec 2025 18:17:45 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2025/12/tp-link_hs110.png" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2025/12/tp-link_hs110.png" alt="TP-Link HS100 - Godsend for Smart Home Integration"><p>Many companies discover their best products unintentionally. WhatsApp began as a simple status-update app, and only after a large portion of users started using status messages to communicate did the team realize messaging was the real value. Slack started as a multiplayer game called Glitch, but the founders soon noticed that the internal chat tool they had built for the team was far more useful than the game itself. Post-it Notes originated from a failed attempt to create a strong adhesive, which unexpectedly became a wildly successful product. You get the idea.</p><p>The TP-Link HS1xx line only rose to fame among home-lab enthusiasts and hackers after <a href="https://blog.georgovassilis.com/2016/05/07/controlling-the-tp-link-hs100-wi-fi-smart-plug/" rel="noreferrer">George Georgovassilis</a> inspected its network traffic with Wireshark. Hidden in plain sight was exactly what people had been wishing for: a simple, easy-to-use API. That discovery transformed the device from an ordinary smart plug into a cult favorite.</p><p>Here&apos;s a tiny Java implementation you might find useful. If you decide to try it, let us know in the comments what you&#x2019;re controlling. At wasteofserver, we use it to manage several electric heaters. A bunch of ESP32 continuously monitors room temperature through thermal sensors, based on that data we control HS100 plugs to switch the heaters on or off as needed. This gives us far more consistent temperature control than the heaters&apos; built-in thermostats can provide.</p><p>We&apos;ll share that code eventually, but for now, <a href="https://amzn.to/4oy9L6E" rel="noreferrer">go grab a few HS110 plugs, recently rebranded Kasa Smart Plug</a>, and start automating your home! &#x1F609;</p><figure class="kg-card kg-image-card kg-card-hascaption"><a href="https://amzn.to/445n2wu"><img src="https://wasteofserver.com/content/images/2025/12/tp-link-kasa-smart-wi-fi-plug-slim-ep25p4_vanz.png" class="kg-image" alt="TP-Link HS100 - Godsend for Smart Home Integration" loading="lazy" width="1280" height="720" srcset="https://wasteofserver.com/content/images/size/w600/2025/12/tp-link-kasa-smart-wi-fi-plug-slim-ep25p4_vanz.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/12/tp-link-kasa-smart-wi-fi-plug-slim-ep25p4_vanz.png 1000w, https://wasteofserver.com/content/images/2025/12/tp-link-kasa-smart-wi-fi-plug-slim-ep25p4_vanz.png 1280w" sizes="(min-width: 720px) 720px"></a><figcaption><span style="white-space: pre-wrap;">HS100, HS105, HS110 now Kasa Smart Plug... the best unannounced feature? Easy API control!</span></figcaption></figure><pre><code class="language-java">package org.frankie.util;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Base64;

public class TpLinkClient {

    private static final String PAYLOAD_ON = &quot;AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu36Lfog==&quot;;
    private static final String PAYLOAD_OFF = &quot;AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu3qPeow==&quot;;
    private static final String PAYLOAD_SYSINFO = &quot;AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr&quot;;
    private static final String PAYLOAD_EMETER = &quot;AAAAJNDw0rfav8uu3P7Ev5+92r/LlOaD4o76k/6buYPtmPSYuMXlmA==&quot;;
    private static final int DEFAULT_HS100_PORT = 9999;


    public static String on(String ip) throws IOException {
        return sendToPlug(ip, DEFAULT_HS100_PORT, PAYLOAD_ON);
    }

    public static String off(String ip) throws IOException {
        return sendToPlug(ip, DEFAULT_HS100_PORT, PAYLOAD_OFF);
    }

    public static String sys_info(String ip) throws IOException {
        return sendToPlug(ip, DEFAULT_HS100_PORT, PAYLOAD_SYSINFO);
    }

    public static String emeter(String ip) throws IOException {
        return sendToPlug(ip, DEFAULT_HS100_PORT, PAYLOAD_EMETER);
    }


    /**
     * Sends a Base64 encoded command to the plug and returns the raw response bytes.
     * &lt;p&gt;
     * Method is synchronized as IoT hardware is generally low-spec, and there&apos;s really not
     * a valid case for concurrent access
     *
     * @param ip            The IP address of the plug
     * @param port          The port (usually 9999)
     * @param base64Payload The Base64 encoded command string
     * @return The response from the plug
     */
    public static synchronized String sendToPlug(String ip, int port, String base64Payload) throws IOException {
        byte[] commandBytes = Base64.getDecoder().decode(base64Payload);

        try (Socket socket = new Socket(ip, port)) {
            socket.setSoTimeout(5000); // Timeout to avoid hanging forever

            try (OutputStream out = socket.getOutputStream();
                 DataInputStream in = new DataInputStream(socket.getInputStream())) {

                out.write(commandBytes);
                out.flush();

                // The TP-Link protocol response starts with a 4-byte big-endian length header.
                // Reading this allows us to know exactly how much data to expect.
                int length = in.readInt();

                byte[] response = new byte[length];
                in.readFully(response);

                return decodeResponse(response);
            }
        }
    }


    private static String decodeResponse(byte[] data) {
        int key = 171;
        StringBuilder sb = new StringBuilder();
        for (byte b : data) {
            // MASKING WITH 0xFF IS CRITICAL, treats the byte as unsigned (0 to 255) preventing negative numbers
            int nextKey = b &amp; 0xFF;
            int decrypted = key ^ nextKey;
            key = nextKey;
            sb.append((char) decrypted);
        }
        return sb.toString();
    }

}</code></pre>]]></content:encoded></item><item><title><![CDATA[Java; terminal with ANSI colors in Windows, Linux & IntelliJ]]></title><description><![CDATA[I've run into this problem more times than I’d like, always having to retrace my steps to get things working again; so I’m putting this online as a reference.]]></description><link>https://wasteofserver.com/java-terminal-with-ansi-colors-in-windows-linux-intellij/</link><guid isPermaLink="false">67e9c2e6517e2f0001e4891d</guid><category><![CDATA[java]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Sun, 30 Mar 2025 22:38:47 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2025/03/2025-03-30-23_37_16-.png" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2025/03/2025-03-30-23_37_16-.png" alt="Java; terminal with ANSI colors in Windows, Linux &amp; IntelliJ"><p>If you&apos;re here, you probably already know what ANSI escape sequences are. </p><p>You output something like the code below, and it prints in color:</p><figure class="kg-card kg-code-card"><pre><code class="language-java">System.out.println(&quot;world is black and white&quot;);
System.out.println(&quot;\u001B[34m&quot; + &quot;world has color&quot; + &quot;\u001B[0m&quot;);</code></pre><figcaption><p><span style="white-space: pre-wrap;">You can check the rest of </span><a href="https://gist.github.com/JBlond/2fea43a3049b38287e5e9cefc87b2124" rel="noreferrer"><span style="white-space: pre-wrap;">ANSI codes here</span></a></p></figcaption></figure><p>We don&#x2019;t often manually write <code>System.out</code> directly to the console. More likely, you&#x2019;re using something like Logback to manage your output and have configured <code>logback.xml</code> for color output.</p><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;configuration&gt;
    &lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
        &lt;encoder&gt;
            &lt;pattern&gt;%d{yyyymmdd HH:mm:ss.SSS} %highlight(%-5level) [%blue(%t)] %yellow(%C): %msg%n%throwable&lt;/pattern&gt;
        &lt;/encoder&gt;
    &lt;/appender&gt;

    &lt;root level=&quot;trace&quot;&gt;
        &lt;appender-ref ref=&quot;CONSOLE&quot;/&gt;
    &lt;/root&gt;
&lt;/configuration&gt;</code></pre><figcaption><p><span style="white-space: pre-wrap;">This will give you color! Just not on Windows.</span></p></figcaption></figure><p>To get color to work on Windows, you need to install Jansi:</p><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.fusesource.jansi&lt;/groupId&gt;
    &lt;artifactId&gt;jansi&lt;/artifactId&gt;
    &lt;version&gt;2.4.1&lt;/version&gt;
&lt;/dependency&gt;</code></pre><figcaption><p><span style="white-space: pre-wrap;">Add this to your </span><code spellcheck="false" style="white-space: pre-wrap;"><span>pom.xml</span></code><span style="white-space: pre-wrap;"> dependencies.</span></p></figcaption></figure><p>If you only install AnsiConsole without enabling pass-through, it will stop working in IntelliJ. To get it working on both Windows and IntelliJ, you need to do both; install AnsiConsole on startup and allow codes to pass through.</p><pre><code class="language-java">    public static void main(String[] args) {
        // for colors to work in Windows, linux and IntelliJ
        System.setProperty(&quot;jansi.passthrough&quot;, &quot;true&quot;);
        AnsiConsole.systemInstall();

        log.trace(&quot;trace&quot;);
        log.debug(&quot;debug&quot;);
        log.info(&quot;info&quot;);
        log.warn(&quot;warn&quot;);
        log.error(&quot;error&quot;);
    }</code></pre><p>Hope it helps. I, for sure, know that&apos;ll I&apos;ll be here in a couple of months, rechecking how to do this once again! ;)</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2025/03/2025-03-30-23_37_16--1.png" class="kg-image" alt="Java; terminal with ANSI colors in Windows, Linux &amp; IntelliJ" loading="lazy" width="797" height="391" srcset="https://wasteofserver.com/content/images/size/w600/2025/03/2025-03-30-23_37_16--1.png 600w, https://wasteofserver.com/content/images/2025/03/2025-03-30-23_37_16--1.png 797w" sizes="(min-width: 720px) 720px"></figure>]]></content:encoded></item><item><title><![CDATA[Zoom M3 MicTrak file recovery]]></title><description><![CDATA[How a series of inconspicuous events lead to a journey deep into the binary encoding of RIFF files and this data recovery tool]]></description><link>https://wasteofserver.com/zoom-m3-mictrak-file-recovery/</link><guid isPermaLink="false">67bb9cadc9c8c40001d8cbd7</guid><category><![CDATA[data-recovery]]></category><category><![CDATA[programming]]></category><category><![CDATA[java]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Tue, 25 Feb 2025 04:18:31 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2025/02/zoom-m3-mictrak.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F3A4;</div><div class="kg-callout-text">If you&apos;re in a hurry, head <a href="https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover" rel="noreferrer">directly to the Github repository</a>.</div></div><img src="https://wasteofserver.com/content/images/2025/02/zoom-m3-mictrak.jpg" alt="Zoom M3 MicTrak file recovery"><p></p><p>While I&apos;m fortunate enough to develop software for state-of-the-art companies, I&apos;ve always been the &quot;printer guy&quot;, a badge I wear with pride.</p><p>And maybe that&apos;s the reason why, for more than a quarter of a century, people have turned up with storage medium to salvage. I never did it professionally, it&#x2019;s just something I enjoy and, more often than not, I&#x2019;m able to help.</p><h3 id="the-process">The process</h3><p></p><p>My main approach is two-fold:</p><ul><li>capture what you can from the dying medium</li><li>try to recreate the data</li></ul><p>Sometimes, just by adjusting read tolerance, you&apos;re able to capture everything from a disk Windows or Mac reject, as the OS times out faster than the dying medium can respond. I&apos;ve had a disk hooked to a box for 7 months; 100% recovery success!</p><p>Sometimes you&apos;ll have chunks of data missing, but one of the File Allocation Table / Master File Table / Container superblock / root B-tree are still there and so most data is salvageable with both filenames and tree location.</p><p>At the extreme, you&#x2019;re left with nothing but some of the raw data. All you can do is carve it out - essentially reading every byte from the medium, identifying known headers like JPG (<code>FF D8</code>) or MKV (<code>1A 45 DF A3</code>), and proceed to capture all sequential data until the end of the file. If for any reason, the file is fragmented, carving will obviously fail.</p><h3 id="the-call-from-pierre-zago">The call from Pierre Zago</h3><p></p><blockquote>Frankie, this hasn&apos;t happened before, I&apos;m generally cautious... I don&apos;t know what I was thinking - I&apos;ve accidentally formatted an SD card with audio for an entire session. Worse, I&apos;ve saved some files onto it!</blockquote><p>Pierre is an incredibly talented comedian and an extraordinary actor. While his work is multifaceted, he became widely known for street sketches, some of them absolutely universal. Just check the one below where he simply says &quot;excuse-me&quot;.</p><figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="200" height="113" src="https://www.youtube.com/embed/JC-yra6dtzw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="COM LICENC&#x327;A"></iframe><figcaption><p><span style="white-space: pre-wrap;">This sketch&#x2019;s simplicity and comedic charm create a lighthearted yet universally appealing work of art.</span></p></figcaption></figure><p>With that simple action, Pierre had joined the club. Everyone messes up. <a href="https://screenrant.com/toy-story-2-movie-deleted-accident-recovered/" rel="noreferrer">Even Pixar</a>. Don&apos;t worry, I said, while taking the card, the overwritten contents are gone, but given that it&apos;s a microphone formatted card, even without FAT tables we should be able to carve out most of whatever you&apos;ve recorded as data will probably be sequential.</p><p>Little did I know, this would turn out to be one of the most interesting toy projects I&#x2019;ve worked on in a while. The last time I had this much fun was rebuilding a hardware RAID-0 that contained the masters for an album by &apos;<a href="https://www.youtube.com/watch?v=zw5l0x3QDnc" rel="noreferrer">Os Azeitonas</a>&apos;.</p><h3 id="the-echo-echo-echo">The Echo (echo, echo)...</h3><p><br>As expected, the only way to get data back was by carving it out of the image dump. There&apos;s a multitude of tools to choose from, <a href="https://www.cgsecurity.org/wiki/photoRec" rel="noreferrer">Photorec</a> (open source), <a href="https://www.ccleaner.com/recuva" rel="noreferrer">Recuva</a> (watch out for bundleware), <a href="https://www.reclaime.com/" rel="noreferrer">ReclaiMe</a> (paid), etc... though I&apos;m partial to <a href="https://www.r-studio.com/data-recovery-software/" rel="noreferrer">R-Studio</a> (paid); their results consistently outperform the competition.</p><p>In this instance, nonetheless, things wouldn&apos;t be so simple. Every software tried was able to extract the <code>wav</code> files, but there was something rather wrong with the data as they all had this repetition, an echo of sorts.</p><p>I&apos;m a bit dull of hearing, so opened them in <a href="https://www.audacityteam.org/" rel="noreferrer">Audacity</a> to check what was going on. You can clearly see a pattern here:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-24-18_38_55-015.png" class="kg-image" alt="Zoom M3 MicTrak file recovery" loading="lazy" width="1714" height="344" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-24-18_38_55-015.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/2025-02-24-18_38_55-015.png 1000w, https://wasteofserver.com/content/images/size/w1600/2025/02/2025-02-24-18_38_55-015.png 1600w, https://wasteofserver.com/content/images/2025/02/2025-02-24-18_38_55-015.png 1714w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">There&apos;s somewhat of a repetition every ~ 0.7 seconds</span></figcaption></figure><p>While the chunks have a pretty similar waveform - there isn&apos;t any sort of binary correlation. Besides, there was also something murky, every <code>wav</code> file had what appeared to be 2 consecutive headers.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-25-01_58_36-HxD----F__68_pierre_zago.img-.png" class="kg-image" alt="Zoom M3 MicTrak file recovery" loading="lazy" width="1158" height="528" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-25-01_58_36-HxD----F__68_pierre_zago.img-.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/2025-02-25-01_58_36-HxD----F__68_pierre_zago.img-.png 1000w, https://wasteofserver.com/content/images/2025/02/2025-02-25-01_58_36-HxD----F__68_pierre_zago.img-.png 1158w" sizes="(min-width: 720px) 720px"></figure><p>Mind you, at this point, my knowledge of <code>wav</code> files was that the header is <code>52 49 46 46</code>. Aside from using a mic, I didn&apos;t query Pierre on how he had actually recorded the data. However, when I saw &quot;ZOOM M3&quot; tag in the header, I called the authority on everything sound.</p><h3 id="perfect-pitch-a-sort-of-wizardry">Perfect Pitch, a sort of wizardry</h3><p><br>Ed&apos;s been on speed dial for the better part of this lifetime. I&apos;m lucky like that. Beyond being an unbelievable composer - just listen to the breathtaking <a href="https://open.spotify.com/artist/3dFVFQhJ7koYGID3rnygYv?si=tjp6im06SrmKup4hISHIvw" rel="noreferrer">Best Youth</a> - he&apos;s also a pitch-perfect, living and breathing, encyclopedia on sound.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/ZGtOOPJKfYA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="Best Youth - Eternal Love"></iframe></figure><blockquote>Zoom? Yes, yes. I have one. They record both wav and raw. Data is mangled? Ah. Sure. Recover? Twisted files? That&apos;s going to be an impossible task and, even if it&apos;s not, it&apos;ll be easier to just re-record.</blockquote><p>And there he had me. It would definitely be easier to just re-record, even Pierre at one point suggested doing a voice-over, but where would the fun be in that?</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/M3_top.jpg" class="kg-image" alt="Zoom M3 MicTrak file recovery" loading="lazy" width="1200" height="430" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/M3_top.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/M3_top.jpg 1000w, https://wasteofserver.com/content/images/2025/02/M3_top.jpg 1200w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Zoom M3 MicTrak</span></figcaption></figure><h3 id="the-magic-number">The Magic Number</h3><p><br>I didn&apos;t even appreciate the concept of a <code>raw</code> wave until Ed explained it, but now I knew the mic saves two simultaneous files, everything fell into place.</p><p>It&apos;s common for digital cameras to store both formats, but a microphone is a different beast. There&apos;s no way to determine upfront how long the user will record, which wouldn&apos;t be an issue if this were a single stream. As it&apos;s storing both tracks, there is quite a bit more play involved.</p><p>You see, as soon as you hit record, the mic creates two files and continuously flushes the captured data from both onto the card. That, is done in chunks of data. One for the RAW, another for the WAV, repeat.</p><p>Since we need to isolate those slices of data in order to untangle it, <strong>we must know their exact size</strong>. And there lies <strong>our magic number</strong>!</p><p>My first thought was that the chunks might be aligned with <code>exFAT</code> allocation unit size. In this case, <code>128 KBytes</code>. Let&apos;s test it.</p><p>The header, clearly states that this is a stereo file (2 channels), recorded at 32 bits per channel, sampled 48k times per second. If you remember from the image above, the chunks repeat at approximately 0.7 seconds.</p><p>Let&apos;s get a rough approximation on the chunk bytes so we know the ballpark.</p><figure class="kg-card kg-code-card"><pre><code class="language-terminal">1 second of data = 2 channels * 32 bits * 48000 samples
1 second of data = 384000 bytes
0.7 seconds ~ 268800 bytes</code></pre><figcaption><p><span style="white-space: pre-wrap;">We&apos;re looking into chunks of around 268 KBytes.</span></p></figcaption></figure><p>And just like that, the idea that data may be chunked to exFAT AUS of 128 Kbytes is instantly disproved.</p><p>The next obvious step would be to travel upwards on base 2. Given that 4096 is a good balance for buffers, let&apos;s evaluate from there:</p><pre><code class="language-terminal">4096 * 32 = 131072 (falls short by about 1/2)
4096 * 64 = 262144 (is in the ballpark of what we&apos;re expecting)

262144/384000 ~ 0.682 seconds of data
</code></pre><p>0.682 seconds matched our estimate of 0.7 seconds so perfectly that I immediately knew <strong>262144</strong> was the constant we were after.</p><h3 id="the-reconstruction">The reconstruction</h3><p><br>Conceptually, the problem was solved. Now it was just a matter of building the tool. For that it would be necessary to:</p><ul><li><strong>Untangle the files</strong> directly from the image dump. Since pieces recovered by other carving software would be half the expected size (the data chunk contains two files, so it&#x2019;s actually double the size reported).</li><li><strong>Learn how to create a RIFF header</strong>.</li><li><strong>Create a RIFF header with a BEXT chunk</strong> to make the recovered files  compatible with the &quot;M3 ZOOM Edit &amp; Play&quot; software.</li></ul><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x2328;&#xFE0F;</div><div class="kg-callout-text"><a href="https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover" rel="noreferrer"><b><strong style="white-space: pre-wrap;">All the code is on GitHub</strong></b></a><b><strong style="white-space: pre-wrap;">!</strong></b></div></div><p><br>And I&apos;m pretty sure you&apos;ll feel more at home there.</p><p>However, for the sake of Google indexing, I&#x2019;m leaving the methods that create both the RIFF and BEXT headers here, something I couldn&#x2019;t find, which unfortunately made the process take longer than I&#x2019;d like to admit.</p><pre><code class="language-java">public class RiffFile {

    /**
     * Creates a RIFF header with BEXT and fmt chunks
     *
     * @param sampleRate    the sample rate of the audio (8000Hz, 44100Hz, 48000Hz, etc) times per second the audio is sampled
     * @param bitsPerSample the bits per sample (8bits, 16bits, 32bits, etc)
     * @param channels      the number of channels (1 mono, 2 stereo, etc)
     * @param audioDataSize the size of the audio data in bytes
     * @return the RIFF header
     * @throws IOException if an I/O error occurs
     */
    public static byte[] createRiffHeader(int sampleRate, short bitsPerSample, short channels, int audioDataSize) throws IOException {
        // calculate the byte rate, block align and file size
        int byteRate = sampleRate * channels * bitsPerSample / 8;
        short blockAlign = (short) (bitsPerSample * channels / 8);

        // stream that will carry the new RIFF file
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(byteArrayOutputStream);

        // riff header
        out.writeBytes(&quot;RIFF&quot;);
        out.writeInt(Integer.reverseBytes(0));
        out.writeBytes(&quot;WAVE&quot;);                             // 9-12 Format always WAVE

        // bext chunk
        writeBextChunk(out);

        // fmt chunk
        out.writeBytes(&quot;fmt &quot;);                             // 13-16 chunkID is &quot;fmt &quot; with trailing whitespace
        out.writeInt(Integer.reverseBytes(16));             // 17-20 size of this chunk, is 16 byts
        out.writeShort(Short.reverseBytes((short) 3));      // 21-22 (2 bytes) audioFormat (1 PCM integer, 3 IEEE 754 float)
        out.writeShort(Short.reverseBytes(channels));       // 23-24 (2 bytes) numChannels (1 mono, 2 stereo, 4, etc)
        out.writeInt(Integer.reverseBytes(sampleRate));     // 25-28 (4 bytes) sampleRate (8000, 44100, 48000, etc)
        out.writeInt(Integer.reverseBytes(byteRate));       // 29-32 (4 bytes) byteRate (sampleRate * numChannels * bitsPerSample/8)
        out.writeShort(Short.reverseBytes(blockAlign));     // 33-34 (2 bytes) blockAlign (numChannels * bitsPerSample/8)
        out.writeShort(Short.reverseBytes(bitsPerSample));  // 35-36 (2 bytes) bitsPerSample (8bits, 16bits, 32bits, etc)

        // data chunk
        out.writeBytes(&quot;data&quot;);                             // 37-40 chunkID ID is &quot;data&quot;
        out.writeInt(Integer.reverseBytes(audioDataSize));  // 41-44 size of this chunk varies
        out.close();

        // write the full size of the file on the 4-8 bytes
        byte[] outArr = byteArrayOutputStream.toByteArray();
        int size = outArr.length - 8;
        ByteBuffer.wrap(outArr, 4, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(size);
        return outArr;
    }

    private static void writeBextChunk(DataOutputStream out) throws IOException {
        // bext chunk
        out.writeBytes(&quot;bext&quot;);
        out.writeInt(Integer.reverseBytes(256 + 32 + 32 + 10 + 8 + 8 + 8 + 2 + 180 + 4 + 4 + 4 + 4 + 4 + 180)); // bext chunk size (fixed size for BWF)

        // description 256 bytes
        writeToArray(out, 256, &quot;&quot;);                     // 256 bytes description
        writeToArray(out, 32, &quot;ZOOM M3&quot;);               // 32 bytes originator
        writeToArray(out, 32, &quot;&quot;);                      // 32 bytes originator reference
        writeToArray(out, 10, &quot;2023-10-01&quot;);            // 10 bytes origination date
        writeToArray(out, 8, &quot;12:00:00&quot;);               // 8 bytes origination time
        writeToArray(out, 8, &quot;12:00:00&quot;);               // 8 bytes time reference

        out.writeLong(Long.reverseBytes(0L));           // 8 bytes time reference
        out.writeShort(Short.reverseBytes((short) 0));    // 2 bytes version
        out.write(new byte[180]);                         // 180 bytes UMID
        out.writeFloat(0.0f);                          // 4 bytes loudness value
        out.writeFloat(0.0f);                          // 4 bytes loudness range
        out.writeFloat(0.0f);                          // 4 bytes max true peak level
        out.writeFloat(0.0f);                          // 4 bytes max momentary loudness
        out.writeFloat(0.0f);                          // 4 bytes max short term loudness

        // zoom m3 needs this bit to allow file to be read from &quot;zoom m3 edit &amp; play&quot;
        writeToArray(out, 180, &quot;A=PCM,F=48000,W=32,M=stereo,T=M3;VERSION=1.00;MSRAW=ON ;&quot;);
    }
    
}</code></pre><p>As you may see, there wasn&apos;t much effort put into the BEXT chunk; I simply created it to ensure &quot;Zoom M3 Edit &amp; Play&quot; would be compatible.</p><h3 id="the-tool">The Tool </h3><p><br>I hope you had an interesting read. I tried to lift the curtain on the thought process while keeping this engaging. The actual code is hopefully self-explanatory, and you will find it here:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/wasteofserver/zoom_m3_mic_wav_data_recover"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - wasteofserver/zoom_m3_mic_wav_data_recover: A data recovery tool for Zoom M3 MicTrak Stereo Shotgun Microphone/Recorder</div><div class="kg-bookmark-description">A data recovery tool for Zoom M3 MicTrak Stereo Shotgun Microphone/Recorder - wasteofserver/zoom_m3_mic_wav_data_recover</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Zoom M3 MicTrak file recovery"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">wasteofserver</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/6ccf8f24b9e4164d619b1b76594e524816d601d6d3d93077f3c198eb2e3e4806/wasteofserver/zoom_m3_mic_wav_data_recover" alt="Zoom M3 MicTrak file recovery"></div></a></figure><p>The challenge wasn&apos;t just about recovering lost recordings - it was about understanding why traditional tools failed and developing a method that worked.</p><p>While it might have been easier to re-record, the thrill of solving the puzzle made the effort worthwhile. In the end we got a custom-built solution that successfully restores Zoom M3 MicTrak recordings.</p><p>If you ever find yourself in a similar situation, hopefully, this breakdown helps you out. And if not, well, at least you got to enjoy a little adventure into the world of data recovery.</p>]]></content:encoded></item><item><title><![CDATA[Permanently delete unwanted emails from Gmail. Out of sight, out of mind!]]></title><description><![CDATA[Getting unwanted emails from a specific sender and can’t resist checking the trash? This guide will help you remove them for good.]]></description><link>https://wasteofserver.com/permanently-delete-unwanted-emails-from-gmail-out-of-sight-out-of-mind/</link><guid isPermaLink="false">67b7ec88c9c8c40001d8ca8b</guid><category><![CDATA[privacy]]></category><category><![CDATA[life hack]]></category><category><![CDATA[trivial]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Fri, 21 Feb 2025 05:07:59 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2025/02/A-hyperrealistic-image-of-a-woman-with-an-email-going-into-the-trash--with-the-trash-on-fire--symbolizing-the-complete-removal-of-harmful-messages-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2025/02/A-hyperrealistic-image-of-a-woman-with-an-email-going-into-the-trash--with-the-trash-on-fire--symbolizing-the-complete-removal-of-harmful-messages-1.png" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!"><p>This post is a bit different from my usual content. While it includes a snippet of code, it&apos;s designed for a less technical audience, thus the flood of screenshots.</p><p>I received an atypical cry for help. A couple is splitting and one of the partners is spamming the other with abusive emails. The receiver created a Gmail filter to trash the messages, but Gmail trash retention policy is 30 days and, in moments of weakness, can&apos;t resist clicking in the trash and reading them, which understandably exacerbates the issue.</p><p>Given the violent nature of the emails, I didn&#x2019;t want to risk deleting potential evidence, so my suggestion was to create a process that would:</p><ul><li>Auto-forward the emails from that sender to a lawyer</li><li>Delete those emails and purge them from trash, bypassing Gmail&apos;s 30-day retention policy</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/A-hyperrealistic-image-of-a-woman-with-an-email-going-into-the-trash--with-the-trash-on-fire--symbolizing-the-complete-removal-of-harmful-messages.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/A-hyperrealistic-image-of-a-woman-with-an-email-going-into-the-trash--with-the-trash-on-fire--symbolizing-the-complete-removal-of-harmful-messages.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/A-hyperrealistic-image-of-a-woman-with-an-email-going-into-the-trash--with-the-trash-on-fire--symbolizing-the-complete-removal-of-harmful-messages.png 1000w, https://wasteofserver.com/content/images/2025/02/A-hyperrealistic-image-of-a-woman-with-an-email-going-into-the-trash--with-the-trash-on-fire--symbolizing-the-complete-removal-of-harmful-messages.png 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Out of sight, out of mind can be the best policy.</span></figcaption></figure><h3 id="create-a-forward-address">Create a forward address</h3><p>On this particular case, the forward address will be the lawyer. Go to Gmail settings, select &quot;Forwarding and POP/IMAP&quot; and click on &quot;Add a forwarding address&quot;.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-03_26_22-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="898" height="389" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-03_26_22-Window.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-03_26_22-Window.png 898w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Remember to keep forwarding DISABLED, we only want to forward emails from a single sender</span></figcaption></figure><p>After adding a forward address, Gmail will send a confirmation to that email - your lawyer - asking for permission. As soon as that&apos;s granted, you may proceed to the next step. Just remember to keep <code>Forwarding</code> disabled!</p><h3 id="create-a-new-filter">Create a new Filter</h3><p>Here we will be specifying that all emails that come from <code>someone@example.com</code> will be forwarded to <code>laywer@example.com</code> and then sent to the trash.</p><p>Go to &quot;Filters and Blocked Addresses&quot; and then click on &quot;Create a new filter&quot;.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-03_32_29-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="1009" height="354" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-03_32_29-Window.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/2025-02-21-03_32_29-Window.png 1000w, https://wasteofserver.com/content/images/2025/02/2025-02-21-03_32_29-Window.png 1009w" sizes="(min-width: 720px) 720px"></figure><p>The form for adding filters will open. You want <strong>all messages</strong> from that specific address to be filtered, so just add &quot;someone@example.com&quot; to the filter and choose &quot;Create filter&quot;.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-03_40_34-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="1006" height="415" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-03_40_34-Window.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/2025-02-21-03_40_34-Window.png 1000w, https://wasteofserver.com/content/images/2025/02/2025-02-21-03_40_34-Window.png 1006w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">You can be picky with filters, but here we want ALL emails from sender to match</span></figcaption></figure><p>Now you&apos;ll have to select exactly what you want the filter to do. On our specific case, we&apos;ll forward the email to the lawyer, so tick that box. We also want to delete the email, so also tick that one.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-03_42_59-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="846" height="631" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-03_42_59-Window.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-03_42_59-Window.png 846w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">And just like that, emails from this sender are forwarded and trashed!</span></figcaption></figure><p>This solves half of the issue and is the most straightforward part of the process. The tricky bit comes next, how to instantly purge those emails from the trash so that we&apos;re not tempted to read them. Google Apps Script to the rescue!</p><h3 id="create-a-google-apps-script">Create a Google Apps Script</h3><p>Google Drive offers a feature that allows you to host and run scripts. While most developers are familiar with this, power users may not be aware of it. For the task at hand, this feature is absolutely perfect.</p><p>Head into <a href="https://script.google.com/">https://script.google.com/</a>, follow the authentication procedures, if needed, and then click on &quot;New Project&quot;.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-03_58_01-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="919" height="551" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-03_58_01-Window.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-03_58_01-Window.png 919w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Hurray for your first script!</span></figcaption></figure><p>You&apos;re now on your project screen. You&apos;ll need to interact with Gmail so let&apos;s add that service. Click on the big <code>+</code> next to &quot;Services&quot;, look for <code>Gmail API</code> and add it.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-04_00_20-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="921" height="611" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-04_00_20-Window.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-04_00_20-Window.png 921w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Your script needs the Gmail API to access your emails</span></figcaption></figure><p>Now, replace the <code>myFunction</code> with this piece of code. Remember, <strong>YOU MUST CHANGE someone@example.com</strong> to the actual address you want to remove from trash!</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">function deleteMailsFromTrash() {
  var gmailSearchString = `in:trash from:someone@example.com`

  var threads = GmailApp.search(gmailSearchString);
  const n = threads.length;
  if (n &lt;= 0) {
    Logger.log(&quot;No threads matching search string \&quot;%s\&quot;&quot;, gmailSearchString);
    return
  } else {
    Logger.log(&quot;%s threads matching action **%s**&quot;, n, gmailSearchString);
  }

  for (var i = 0; i &lt; threads.length; i++) {
    var thread = threads[i];
    Logger.log(`\t Thread# ${i} [ID: ${thread.getId()}]: [message : ${thread.getFirstMessageSubject()}] deleted`);
    Gmail.Users.Threads.remove(&apos;me&apos;, thread.getId());
  }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">This script will look in the trash for emails from </span><code spellcheck="false" style="white-space: pre-wrap;"><span>someone@example.com</span></code><span style="white-space: pre-wrap;"> and delete them</span></p></figcaption></figure><p>Your screen should now resemble this. Go ahead and rename &quot;Untitled project&quot; to something more meaningful, like &quot;Purge Specific Mails from Trash&quot;. Also change <code>myFunction</code> to <code>deleteMailsFromTrash</code> and then hit <code>Run</code>.</p><p>You&apos;ll be asked to give permissions to access your Google account.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-04_23_22-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="1144" height="433" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-04_23_22-Window.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/2025-02-21-04_23_22-Window.png 1000w, https://wasteofserver.com/content/images/2025/02/2025-02-21-04_23_22-Window.png 1144w" sizes="(min-width: 720px) 720px"></figure><p>Now you&apos;ll get an error! Google hasn&apos;t verified this app. <em>While the developer hasn&apos;t verified the app, you shouldn&apos;t use.</em> In this particular case, <strong>you are the developer</strong>! That&apos;s why I didn&apos;t release this solution as a pre-made script. It&apos;s safer to have the code running on your side.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-04_24_40-Window-1.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="639" height="435" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-04_24_40-Window-1.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-04_24_40-Window-1.png 639w"></figure><p>Click on that &quot;Go to Purge Specific Mails from Trash (unsafe)&quot; link and then continue. On your Apps Script window, you&apos;ll see the first execution of your script.</p><p>On my case, since I don&apos;t have email from <code>someone@example.com</code> on the &quot;trash&quot; the program simply prints a &quot;No threads matching search string&quot;. On your specific case, you may see that a couple of emails have been deleted! Well done.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-04_30_43-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="1148" height="571" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-04_30_43-Window.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/2025-02-21-04_30_43-Window.png 1000w, https://wasteofserver.com/content/images/2025/02/2025-02-21-04_30_43-Window.png 1148w" sizes="(min-width: 720px) 720px"></figure><p>Now everything is working, but we still need to set up a trigger to run the script automatically, ensuring unwanted emails are deleted in a timely manner.</p><h3 id="configure-a-trigger">Configure a trigger!</h3><p>You&apos;ll want a time driven trigger. Something that runs on a schedule making sure that the emails that were placed in the trash by the filter you created above are purged before you can get to them.</p><p>Click on the clock on the left sidebar, then on the big blue button on the bottom right that says &quot;Add Trigger&quot; and configure the filter as per the image below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-04_37_40-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="873" height="796" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-04_37_40-Window.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-04_37_40-Window.png 873w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">And there you have it! Process is automated.</span></figcaption></figure><p>I&#x2019;ve set this script to run every 5 minutes, but you can adjust the interval to as low as 1 minute if you prefer. Tweak as needed. Setting a longer interval is simply a way to considerate of Google&apos;s infrastructure.</p><p>Now, to make sure things are working as expected, on the left sidebar, click on executions. You&apos;ll see a table with all the times the script has run. As you&apos;ve just implemented it, there should probably be two to three runs. One manual <code>Type: Editor</code> from when you manually executed it and then a couple more from the time-driven trigger labeled with <code>Type: Time-Driven</code>. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/2025-02-21-04_49_17-Window.png" class="kg-image" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!" loading="lazy" width="874" height="350" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/2025-02-21-04_49_17-Window.png 600w, https://wasteofserver.com/content/images/2025/02/2025-02-21-04_49_17-Window.png 874w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Peace.</span></figcaption></figure><p>As challenging as it may be to navigate these situations, it&#x2019;s important to remember that protecting your health is a priority. </p><p>While technology can help us minimize harmful distractions, healing takes time and self-compassion. Stay strong, take care of yourself, and don&#x2019;t hesitate to seek support. </p><p>You deserve peace and healing through this process.</p><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/4kaAc1p"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2025/02/81SMG0mk-yL._AC_SL1500_.jpg" alt="Permanently delete unwanted emails from Gmail. Out of sight, out of mind!"></a>
  </div>
  <div class="text">
    <p>
      I don&#x2019;t know if you&#x2019;ve tried rucking before, but it&#x2019;s something that liberates my mind. <br> <br>It&#x2019;s simply <a href="https://amzn.to/4kaAc1p">walking while carrying weight</a>. Originally a military exercise, rucking is gaining popularity for its benefits to physical health, stability, and mental well-being. Give it a try!
    
    </p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Stop 404 prying bots with HAProxy]]></title><description><![CDATA[If you manage a public facing web server, you'll have noticed that bots try their luck on a myriad of non-existing pages. Leverage HAProxy to stop them at the door.]]></description><link>https://wasteofserver.com/stop-404-prying-bots-with-haproxy/</link><guid isPermaLink="false">67b28e8ac9c8c40001d8c976</guid><category><![CDATA[haproxy]]></category><category><![CDATA[security]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Mon, 17 Feb 2025 06:09:16 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2025/02/bot_tries_to_404_a_server.png" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2025/02/bot_tries_to_404_a_server.png" alt="Stop 404 prying bots with HAProxy"><p>Your logs are filled with 404 hits on <code>/.DS_Store</code>, <code>/backup.sql</code>, <code>/.vscode/sftp.json</code> and a multitude of other URLs. While these requests are mostly harmless, unless of course, your server actually does have something to offer at those locations, you should placate the bots.</p><p><strong>Why?</strong> </p><p>Hitting a server is a resource intensive task and, given that those bots have an extensive list of different URLs, there&apos;s no caching mechanism that can help you. Besides, stopping bots is always a safety measure.</p><p>We&apos;ve previously used HAProxy to <a href="https://wasteofserver.com/how-to-prevent-attacks-on-wordpress-when-running-under-cloudflare/" rel="noreferrer">mitigate attacks on Wordpress login page</a>, the idea is to extend that approach to also cover 404 errors. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/02/bots_wait_their_turn_before_trying_to_enter_a_server.png" class="kg-image" alt="Stop 404 prying bots with HAProxy" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2025/02/bots_wait_their_turn_before_trying_to_enter_a_server.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/02/bots_wait_their_turn_before_trying_to_enter_a_server.png 1000w, https://wasteofserver.com/content/images/2025/02/bots_wait_their_turn_before_trying_to_enter_a_server.png 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Bots will try their best to create havoc in your server</span></figcaption></figure><p>I&apos;ve taken <a href="https://www.tekovic.com/blog/block-abusive-ips-based-on-404-error-rate-using-haproxy/" rel="noreferrer">inspiration from Sasa Tekovic</a>, particularly in ensuring that legitimate search engine crawlers aren&#x2019;t blocked and allowing 404 errors on static resources. This prevents genuine missing resources - an error that&apos;s bound to happen - from inadvertently blocking legitimate users.</p><p>Before implementing, it&apos;s always good to spin up a local test environment. Let&apos;s start <code>HAProxy</code> and <code>Apache</code> using <code>Docker</code>. We do need an actual backend server to give us those <code>404</code>.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">version : &apos;3&apos;

services:
    haproxy:
        image: haproxy:3.1.3-alpine
        ports:
            - &quot;8100:80&quot;
        volumes:
            - &quot;./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg&quot;
        networks:
            - webnet
    apache:
        image: httpd:latest
        container_name: apache1
        ports:
            - &quot;8080:80&quot;
        volumes:
            - ./html:/usr/local/apache2/htdocs/
        networks:
            - webnet
            
networks:
    webnet:</code></pre><figcaption><p><span style="white-space: pre-wrap;">Then, simply run </span><code spellcheck="false" style="white-space: pre-wrap;"><span>docker-compose up</span></code><span style="white-space: pre-wrap;">, and you can access </span><a href="http://localhost:8100" rel="noreferrer"><code spellcheck="false" style="white-space: pre-wrap;"><span>localhost:8100</span></code></a><span style="white-space: pre-wrap;"> in your browser.</span></p></figcaption></figure><p>The <code>haproxy.cfg</code> file is pretty much self-explanatory:</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">global
    log stdout format raw daemon debug
	
defaults
    log     global
	mode    http

frontend main
    bind *:80
	
	acl static_file path_end .css .js .jpg .jpeg .gif .ico .png .bmp .webp .csv .ttf .woff .svg .svgz
    acl excluded_user_agent hdr_reg(user-agent) -i (yahoo|yandex|kagi|(google|bing)bot)

    # tracks IPs but exclude hits on static files and search engine crawlers
    http-request track-sc0 src table mock_404_tracking if !static_file !excluded_user_agent
    # increment gpc0 if response code was 404
    http-response sc-inc-gpc0(0) if { status 404 }
   	# checks if the 404 error rate limit was exceeded
    http-request deny deny_status 403 content-type text/html lf-string &quot;404 abuse&quot; if { sc0_gpc0_rate(mock_404_tracking) ge 5 }

    # whatever backend you&apos;re using
    use_backend apache_servers

backend apache_servers
    server apache1 apache1:80 maxconn 32

# mock backend to hold a stick table
backend mock_404_tracking
    stick-table type ip size 100k expire 10m store gpc0,gpc0_rate(1m)

</code></pre><figcaption><p><span style="white-space: pre-wrap;">If you get more than 5 hits on 404 requests in a single minute, bot will be banned for 10 minutes.</span></p></figcaption></figure><hr><p>As it stands, this setup effectively rate-limits bots generating excessive 404s. However, we also want to integrate it with our previous example, where we used <code>HAProxy</code> to block attacks on <code>WordPress</code>.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">global
    log stdout format raw daemon debug
	
defaults
    log     global
	mode    http

frontend main
    bind *:80
	
	# We may, or may not, be running this with Cloudflare acting as a CDN.
    # If Cloudflare is in front of our servers, user/bot IP will be in 
    # &apos;CF-Connecting-IP&apos;, otherwise user IP with be in &apos;src&apos;. So we make
    # sure to set a variable &apos;txn.actual_ip&apos; that has the IP, no matter what
    http-request set-var(txn.actual_ip) hdr_ip(CF-Connecting-IP) if { hdr(CF-Connecting-IP) -m found }
    http-request set-var(txn.actual_ip) src if !{ hdr(CF-Connecting-IP) -m found }

    # gets the actual IP on logs
    log-format &quot;%ci\ %hr\ %ft\ %b/%s\ %Tw/%Tc/%Tt\ %B\ %ts\ %r\ %ST\ %Tr IP:%{+Q}[var(txn.actual_ip)]&quot;

    # common static files where we may get 404 errors and also common search engine
    # crawlers that we don&apos;t want blocked
	acl static_file path_end .css .js .jpg .jpeg .gif .ico .png .bmp .webp .csv .ttf .woff .svg .svgz
    acl excluded_user_agent hdr_reg(user-agent) -i (yahoo|yandex|kagi|google|bing)

    # paths where we will rate limit users to prevent Wordpress abuse
	acl is_wp_login path_end -i /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST

	# 404 abuse blocker
    # track IPs but exclude hits on static files and search engine crawlers
    # increment gpc0 counter if response status was 404 and deny if rate exceeded
    http-request track-sc0 var(txn.actual_ip) table mock_404_track if !static_file !excluded_user_agent
    http-response sc-inc-gpc0(0) if { status 404 }
    http-request deny deny_status 403 content-type text/html lf-string &quot;404 abuse&quot; if { sc0_gpc0_rate(mock_404_track) ge 5 }

    # wordpress abuse blocker
    # track IPs if the request hits one of the monitored paths with a POST request
    # increment gpc1 counter if path was hit and deny if rate exceeded
    http-request track-sc1 var(txn.actual_ip) table mock_wplogin_track if is_wp_login is_post
    http-request sc-inc-gpc1(1) if is_wp_login is_post 
    http-request deny deny_status 403 content-type text/html lf-string &quot;login abuse&quot; if { sc1_gpc1_rate(mock_wplogin_track) ge 5 }
	
    # your backend, here using apache for demonstration purposes
    use_backend apache_servers

backend apache_servers
    server apache1 apache1:80 maxconn 32

# mock backends for storing sticky tables
backend mock_404_track
    stick-table type ip size 100k expire 10m store gpc0,gpc0_rate(1m)
backend mock_wplogin_track
    stick-table type ip size 100k expire 10m store gpc1,gpc1_rate(1m)


</code></pre><figcaption><p><span style="white-space: pre-wrap;">Running with two </span><a href="https://www.haproxy.com/documentation/haproxy-configuration-tutorials/core-concepts/stick-tables/"><code spellcheck="false" style="white-space: pre-wrap;"><span>stick tables</span></code></a><span style="white-space: pre-wrap;">, and stopping both threats.</span></p></figcaption></figure><p>And there you have it. HAProxy once again used for much more than as a simple reverse proxy. It&apos;s a little Swiss Knife!</p><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/3CIuG5t"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2025/02/717clWbXnRL._AC_SL1500_.jpg" alt="Stop 404 prying bots with HAProxy"></a>
  </div>
  <div class="text">
    <p>
This headlamp has been a game-changer when working on repairs.
      </p>
    <p>I had one, but when it broke, hesitated to replace it, resorting to the phone&#x2019;s flashlight. And sure, it works - but once <a href="https://amzn.to/3CIuG5t">you experience the convenience of having both hands free again</a> - there&#x2019;s no going back. If you need reliable, hands-free lighting, this is a must-have!
      </p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Postfix using Gmail as a relay server]]></title><description><![CDATA[It's trivial to configure your box to dispatch emails via an external SMTP provider like Gmail.]]></description><link>https://wasteofserver.com/postfix-using-gmail-as-a-relay-server/</link><guid isPermaLink="false">6717b7b2cde1480001aad833</guid><category><![CDATA[email]]></category><category><![CDATA[postfix]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Tue, 22 Oct 2024 15:24:41 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2024/10/A-futuristic-robot-delivering-mail-in-the-style-of-the-US-postal-service-in-the-1900s-2.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Just need to forward emails from your Linux user account to another email? Add a&#xA0;<code spellcheck="false" style="white-space: pre-wrap;">~/.forward</code>&#xA0;file with the destination email (ie. frankie@example.com). Then&#xA0;<code spellcheck="false" style="white-space: pre-wrap;">chmod 644</code>&#xA0;the file.</div></div><img src="https://wasteofserver.com/content/images/2024/10/A-futuristic-robot-delivering-mail-in-the-style-of-the-US-postal-service-in-the-1900s-2.png" alt="Postfix using Gmail as a relay server"><p></p><p>If you have some Linux servers and want them to dispatch your emails (system, logging, monitoring, etc) to an external email address, you can use a proper SMTP service, like Gmail, for that.</p><p>Notice that this <strong>will not work </strong>for mass emails. The idea is to send a few emails per day to yourself. If you&apos;re relaying emails for actual users, you must set up a dedicated mail server or use an external service like <a href="https://www.mailgun.com/" rel="noreferrer">Mailgun</a> or <a href="https://aws.amazon.com/ses/" rel="noreferrer">Amazon Simple Email Service</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/10/US-postal-service-in-the-1900s-1.png" class="kg-image" alt="Postfix using Gmail as a relay server" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2024/10/US-postal-service-in-the-1900s-1.png 600w, https://wasteofserver.com/content/images/size/w1000/2024/10/US-postal-service-in-the-1900s-1.png 1000w, https://wasteofserver.com/content/images/2024/10/US-postal-service-in-the-1900s-1.png 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Copilots rendition of mail, now and then.</span></figcaption></figure><p>I&apos;m assuming:</p><p>1) you want your Linux box to send emails from an existing Gmail account<br>2) you&apos;ve created an &quot;<a href="https://myaccount.google.com/apppasswords" rel="noreferrer"><em>app specific password</em></a>&quot; for this service</p><p>Remove possible previous installations of <code>sendmail</code>, <code>postfix</code>, <code>exim</code> and <code>qmail</code>:</p><pre><code class="language-bash">sudo apt-get remove packagename sendmail postfix exim qmail</code></pre><p>Install <code>postfix</code>:</p><pre><code class="language-bash">sudo apt-get install postfix</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/10/postfix_configure_screen.png" class="kg-image" alt="Postfix using Gmail as a relay server" loading="lazy" width="668" height="460" srcset="https://wasteofserver.com/content/images/size/w600/2024/10/postfix_configure_screen.png 600w, https://wasteofserver.com/content/images/2024/10/postfix_configure_screen.png 668w"><figcaption><span style="white-space: pre-wrap;">When presented with this screen, say that you want &quot;No configuration&quot;</span></figcaption></figure><p>Configure <code>postfix</code> with the values below:</p><pre><code class="language-bash">sudo vim /etc/postfix/main.cf</code></pre><figure class="kg-card kg-code-card"><pre><code class="language-bash"># your preference may vary
inet_protocols = ipv4
inet_interfaces = loopback-only
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination

# your box hostname
myhostname = example-mail-box.wasteofserver.com
mydestination = $myhostname, localhost

# use Gmail with TLS as destination
relayhost = [smtp.gmail.com]:465
smtp_sasl_auth_enable = yes
smtp_use_tls = yes
smtp_tls_wrappermode = yes
smtp_tls_security_level = encrypt
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_sasl_password_maps = hash:/etc/postfix/sasl/sasl_password

# if something goes wrong you may want to uncomment these to check for logs
# on /var/log/mail.log and /var/log/mail.error
#debug_peer_list=smtp.gmail.com
#debug_peer_level=3</code></pre><figcaption><p><span style="white-space: pre-wrap;">Minimum viable </span><code spellcheck="false" style="white-space: pre-wrap;"><span>postfix</span></code><span style="white-space: pre-wrap;"> configuration</span></p></figcaption></figure><p>Create a file with your <code>Gmail account</code> and access credentials:</p><pre><code class="language-bash">sudo vim /etc/postfix/sasl/sasl_password</code></pre><p>With the following content:</p><pre><code class="language-bash">smtp.gmail.com example@gmail.com:your-app-password-here</code></pre><p>And then convert it into a database file that <code>postfix</code> can read:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">sudo postmap /etc/postfix/sasl/sasl_password</code></pre><figcaption><p><span style="white-space: pre-wrap;">This command generates a </span><code spellcheck="false" style="white-space: pre-wrap;"><span>/etc/postfix/sasl/sasl_password.db</span></code><span style="white-space: pre-wrap;"> file that postfix can read</span></p></figcaption></figure><p>Restart the service:</p><pre><code class="language-bash">sudo /etc/init.d/postfix restart</code></pre><p>And now send an email using the command line to check if everything&apos;s working as it should:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">echo &quot;test email&quot; | mailx -a &quot;From: wasteofserver &lt;no-reply@wasteofserver.com&gt;&quot; -s &quot;this is a test email from wasteofserver&quot; youremail@youraccount.com</code></pre><figcaption><p><span style="white-space: pre-wrap;">Hopefully, you&apos;ve got mail!</span></p></figcaption></figure><p>The logs should show something like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">Jun 25 17:14:56 AEC1E9C0043: uid=0 from=&lt;wasteofserver&gt;
Jun 25 17:14:58 AEC1E9C0043: to=&lt;you@gmail.com&gt;, relay=smtp.gmail.com[173.194.76.109]:587, delay=2.2, delays=0.06/0.01/1.4/0.73, dsn=2.0.0, status=sent (250 2.0.0 OK  1624637698 v5sm11709310wml.26 - gsmt
Jun 25 17:14:58 AEC1E9C0043: removed</code></pre><figcaption><p><span style="white-space: pre-wrap;">Notice Gmail as the relay server</span></p></figcaption></figure><p>Alternatively, <strong>you can also check your Gmail &quot;sent folder&quot;</strong>. As the email is sent using your Gmail account (using SMTP), the email will also be there.</p><hr><p><strong>Edit: </strong>Thanks to <a href="https://www.reddit.com/user/kevdogger/">u/kevdogger</a> for pointing that in Arch Linux, the default is no longer B-tree maps but lmdb (Lightning Memory-Mapped Database) and, as such, you will need to change <code>main.cf</code> to point to the correct file.</p><p>The easiest way is to prefix the map with the actual type, something like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># different types of maps created by postmap, change your config file accordingly
smtp_sasl_password_maps = hash:/etc/postfix/sasl/sasl_password
smtp_sasl_password_maps = btree:/etc/postfix/sasl/sasl_password
smtp_sasl_password_maps = lmdb:/etc/postfix/sasl/sasl_password</code></pre><figcaption><p dir="ltr"><code spellcheck="false" style="white-space: pre-wrap;"><span>postmap</span></code><span style="white-space: pre-wrap;"> can create various types of maps, so adjust your configuration accordingly.</span></p></figcaption></figure><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/3BTCsbP"><img style="max-width: 100%; max-height: 290px; margin: 0" src="https://wasteofserver.com/content/images/2024/10/motorola-walkie-talkie.jpg" alt="Postfix using Gmail as a relay server"></a>
  </div>
  <div class="text">
    <p>
      Last Christmas, my wife asked for walkie-talkies. I thought it would be one of those gifts that end up collecting dust, but decided to get her <a href="https://amzn.to/3BTCsbP">a high-quality set anyway</a>. We now use them regularly&#x2014;at least once a month&#x2014;for just about everything. From road trips and hikes to beach outings, these walkie-talkies have become an essential part of our adventures.</p>

<p>When a product genuinely delivers on its promise, it makes all the difference. I can&#x2019;t recommend these enough.
    </p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables]]></title><description><![CDATA[How to restrict a user when the IP you're getting hit with belongs to the CDN (Content Distribution Network)? This is a guide on how to leverage an OSI Level 7 Proxy, such as HAProxy, to scope and filter malicious requests.]]></description><link>https://wasteofserver.com/how-to-prevent-attacks-on-wordpress-when-running-under-cloudflare/</link><guid isPermaLink="false">66eb16c9cde1480001aad4c6</guid><category><![CDATA[haproxy]]></category><category><![CDATA[security]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Thu, 19 Sep 2024 17:04:48 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2024/09/firewall_preventing_attack.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2024/09/firewall_preventing_attack.jpeg" alt="Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables"><p>The most effective way to prevent bots from spamming your server is to drop them at the firewall. This is generally achieved using tools like <a href="https://github.com/denyhosts/denyhosts" rel="noreferrer">Denyhosts</a> or <a href="https://github.com/fail2ban/fail2ban" rel="noreferrer">fail2ban</a>, which monitor your logs, identify suspicious activity, and block the offending IP addresses before they cause harm.</p><p>Denyhosts works at the application level by adding entries to <code>/etc/hosts.deny</code>, whereas fail2ban operates at the firewall level using <code>iptables</code>, which makes it far more efficient.</p><p>However, on resource-constrained machines, fail2ban can still be taxing. A few years ago, we shared a demo of a lightweight log parser called <a href="https://github.com/wasteofserver/banbylog" rel="noreferrer"><strong>banbylog</strong></a>, tailored specifically for our needs (SSH and WordPress activity monitoring) at a much lower resource cost. If that sounds like a good fit, feel free to check it out, but keep in mind that it&#x2019;s not production-ready!</p><p>While the consensus is to parse logs and block hostile IPs at the firewall, <strong>it will not work</strong> <strong>under Cloudflare </strong>umbrella!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/09/firewall_preventing_attack-2.jpeg" class="kg-image" alt="Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables" loading="lazy" width="1000" height="600" srcset="https://wasteofserver.com/content/images/size/w600/2024/09/firewall_preventing_attack-2.jpeg 600w, https://wasteofserver.com/content/images/2024/09/firewall_preventing_attack-2.jpeg 1000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Ideally the attack would stop here, but it won&apos;t work with a full CDN like Cloudflare</span></figcaption></figure><h2 id="why-cant-we-flag-offending-ips-with-cloudflare">Why can&apos;t we flag offending IPs with Cloudflare?</h2><p>Because the IPs that are &quot;attacking&quot; your server are not the actual offending IPs, but Clouflare machines that are proxying the request to your server. Take a look at the diagram below:</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2024/09/server_behind_cdn-1.png" class="kg-image" alt="Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables" loading="lazy" width="759" height="586" srcset="https://wasteofserver.com/content/images/size/w600/2024/09/server_behind_cdn-1.png 600w, https://wasteofserver.com/content/images/2024/09/server_behind_cdn-1.png 759w" sizes="(min-width: 720px) 720px"></figure><p>Cloudflare acts as a middleman between your server and the users. The only IP addresses visible to your firewall are from Cloudflare, not from the original user.</p><p>In fact, you should explicitly whitelist <a href="https://www.cloudflare.com/ips/" rel="noreferrer">Cloudflare IP range</a>. If you happen to block an IP that belongs to Cloudflare, legitimate users will see your site as down.</p><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">&#x1F631;</div><div class="kg-callout-text">While this article&apos;s focus is on securing servers while working under a Content Distribution Network, Cloudflare offers a variety of tools - free and paid - you may leverage to achieve these same results.</div></div><p></p><h3 id="if-the-ip-we-see-doesnt-belong-to-the-user-making-the-request-what-can-we-look-for">If the IP we see doesn&apos;t belong to the user making the request, what can we look for?</h3><p></p><p>Every request Cloudflare sends to your server has an attached header that carries the user original IP under <code>CF-Connecting-IP</code>. And that&apos;s what we can leverage to get them! Unfortunately, reading <code>http</code> headers is too upstream for <code>iptables</code>, thus <strong>HAProxy to the rescue</strong>!</p><p>While <code>iptables</code> operates at Layer 4, <code>HAProxy</code> can operate at OSI Layer 7.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Remember the OSI model:<br><br>Layer 7 - Application; protocols (HTTP, ...)<br>Layer 6 - Presentation; character encoding (ASCII, UTF8, ...)<br>Layer 5 - Session; stick client to server<br>Layer 4 - Transport; protocols (TCP, UDP, ...)<br>Layer 3 - Network; routing protocols (IP)<br>Layer 2 - Data Link; physical to network (ARP, Ethernet)<br>Layer 1 - Physical; cabling, Wi-Fi</div></div><p></p><p>The easiest way to test HAProxy configurations is to boot a Docker instance of HAProxy:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker run --name haproxy -p 888:80 -v ${pwd}:/usr/local/etc/haproxy --sysctl net.ipv4.ip_unprivileged_port_start=0 haproxy:2.3</code></pre><figcaption><p><span style="white-space: pre-wrap;">Start HAProxy listening on host port 888</span></p></figcaption></figure><p>The above assumes you&apos;re using Powershell and are in the directory that contains <code>haproxy.cfg</code>. Change <code>${pwd}</code> accordingly if not.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker kill -s HUP haproxy</code></pre><figcaption><p><span style="white-space: pre-wrap;">This command forces HAProxy restart, which is useful to iterate different configs</span></p></figcaption></figure><p>Below is a straightforward <code>haproxy.cfg</code> that will put HAProxy listening on port <code>80</code>, log to <code>stdout</code> so you can get an instant glimpse on what&apos;s going on, and also print the <code>CF-Connecting-IP</code> header.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">global
    # output logs straight to stdout
    log stdout format raw daemon debug

defaults
    # put HAProxy in Level 7 mode allowing to inspect http protocol
    log     global
    mode    http

frontend  main
    bind *:80
	
    # capture original IP from header and put it in variable txn.cf_conn_ip
    http-request set-var(txn.cf_conn_ip) hdr(CF-Connecting-IP)

    # get the original IP on logs (just to check if things are working)
    log-format &quot;%ci\ %hr\ %ft\ %b/%s\ %Tw/%Tc/%Tt\ %B\ %ts\ %r\ %ST\ %Tr CF-IP:%{+Q}[var(txn.cf_conn_ip)]&quot;

    # use backend ok, which always returns a 200, for debug
    use_backend ok

backend ok
	http-request return status 200 content-type &quot;text/plain&quot; lf-string &quot;ok&quot;</code></pre><figcaption><p><span style="white-space: pre-wrap;">This should be enough to test HAProxy</span></p></figcaption></figure><p></p><p>Let&apos;s make a test request to HAProxy. </p><p>I&apos;m partial to <a href="https://www.usebruno.com/" rel="noreferrer">Bruno</a>, a portable and offline alternative to Postman. Download it, add the <code>CF-Connecting-IP</code> header and make a <code>POST</code> request to <a href="http://localhost:888/wp-login.php"><code>http://localhost:888/wp-login.php</code></a> to test if everything&apos;s working.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/09/bruno_post_request.png" class="kg-image" alt="Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables" loading="lazy" width="988" height="466" srcset="https://wasteofserver.com/content/images/size/w600/2024/09/bruno_post_request.png 600w, https://wasteofserver.com/content/images/2024/09/bruno_post_request.png 988w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Nothing to install, a POST request in under a minute!</span></figcaption></figure><p>If things went as planned, your HAProxy should have printed this:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">172.17.0.1 main ok/&lt;NOSRV&gt; -1/-1/0 78 LR POST /wp-login.php HTTP/1.1 200 -1 CF-IP:&quot;222.222.222.222&quot;</code></pre><figcaption><p><span style="white-space: pre-wrap;">Now that we have the offending IP (222.222.222.222), we can block it!</span></p></figcaption></figure><p>On our particular case, bots are hitting <code>wp-login.php</code>, <code>xmlrpc.php</code> and <code>xmrlpc.php</code> (last one is a typo, but we&apos;ve had more than 100k hits in the last 24h!). We also know that they&apos;re flooding the server with POST requests trying to brute-force passwords.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">frontend main
    # requests that will be monitored and blocked if abused
    acl is_wp_login path_end -i /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST</code></pre><figcaption><p><span style="white-space: pre-wrap;">Add this to end of the </span><code spellcheck="false" style="white-space: pre-wrap;"><span>frontend main</span></code><span style="white-space: pre-wrap;"> block</span></p></figcaption></figure><p>We now have a couple of options:</p><p>a) Now that we have the offending IPs in the log, we could change <code>banbylog</code> to write them to a file, and have HAProxy deny those requests. While this would work, HAProxy would need to be constantly reloaded.</p><p>b) Or we may simply leverage HAProxy stick tables and do a rate-limiting on offending requests.</p><p>While &quot;<em>a&quot;</em> would allow us to ban the offending IP for an indefinite amount of time, <em>&quot;b&quot;</em> has less moving pieces.</p><pre><code class="language-bash">frontend main
    # requests that will be monitored and blocked if abused
    acl is_wp_login path_end -i /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST

    # table than can store 100k IPs, entries expire after 1 minute
	stick-table type ip size 100k expire 1m  store http_req_rate(1m)

    # we&apos;ll track (save to table) the original IP only if the request hits
    # one of the monitored paths with a POST request
    http-request track-sc0 hdr(CF-Connecting-IP) if is_wp_login is_post

    # we now query the stick-table and if the IP has made more than 
    # 5 requests of the offending type in the last minute, 
    # current request is denied
    http-request deny if is_wp_login is_post { sc_http_req_rate(0) gt 5 }

</code></pre><p>HAProxy has <a href="https://www.haproxy.com/blog/use-haproxy-response-policies-to-stop-threats" rel="noreferrer">multiple deny options</a>, tarpit, silent drop, reject or shadowban. A tarpit deny would be something like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># make request hang for 20 seconds before replying back with a 403
timeout tarpit 20s
http-request tarpit deny_status 403 if is_wp_login is_post { sc_http_req_rate(0) gt 2 }</code></pre><figcaption><p><span style="white-space: pre-wrap;">Notice that this puts extra stress on both HAProxy and iptables as they must keep the connection open.</span></p></figcaption></figure><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">When running behind Cloudflare, avoid tarpitting as it puts unnecessary strain on Cloudflare&apos;s infrastructure. Additionally, pay close attention to the status codes your server returns. For instance, sending a &quot;403 - forbidden&quot; indicates that the client lacks permission to access the resource. However, if you send a &quot;500 - internal server error&quot; Cloudflare will reasonably interpret it as an issue with your server, potentially triggering false alerts.</div></div><p></p><p>Just for reference, here&apos;s the full (overly simplified) HAProxy config file that blocks requests if they hit one of the monitored URL&apos;s with more than 5 hits in less than a minute:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">global
    log stdout format raw daemon debug
	
defaults
    log     global
	mode    http

frontend main
    bind *:80
	
    # capture original IP from header and put it in variable txn.cf_conn_ip
    http-request set-var(txn.cf_conn_ip) hdr(CF-Connecting-IP)

    # get the original IP on logs (just to check if things are working)
    log-format &quot;%ci\ %hr\ %ft\ %b/%s\ %Tw/%Tc/%Tt\ %B\ %ts\ %r\ %ST\ %Tr CF-IP:%{+Q}[var(txn.cf_conn_ip)]&quot;

    acl is_wp_login path_end -i /wp-login.php /xmlrpc.php /xmrlpc.php
    acl is_post method POST
	stick-table type ip size 100k expire 1m  store http_req_rate(1m)
	http-request track-sc0 var(txn.cf_conn_ip) if is_wp_login is_post
	http-request deny if is_wp_login is_post { sc_http_req_rate(0) gt 5 }

    # obviously change this to whatever backend you&apos;re using
    use_backend ok

backend ok
	http-request return status 200 content-type &quot;text/plain&quot; lf-string &quot;ok&quot;
</code></pre><figcaption><p><span style="white-space: pre-wrap;">And there you have it, using your proxy as a gatekeeper!</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/09/cpu_usage_wp-login_attack-1.png" class="kg-image" alt="Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables" loading="lazy" width="610" height="239" srcset="https://wasteofserver.com/content/images/size/w600/2024/09/cpu_usage_wp-login_attack-1.png 600w, https://wasteofserver.com/content/images/2024/09/cpu_usage_wp-login_attack-1.png 610w"><figcaption><span style="white-space: pre-wrap;">The CPU toll of a </span><code spellcheck="false" style="white-space: pre-wrap;"><span>wp-login.php</span></code><span style="white-space: pre-wrap;"> brute-force attack and then </span><code spellcheck="false" style="white-space: pre-wrap;"><span>HAProxy</span></code><span style="white-space: pre-wrap;"> acting as a bouncer.</span></figcaption></figure><h2 id="re-captcha-obviously">Re-Captcha, obviously!</h2><p>While this article focus on the server side of things, the first thing you should obviously do, is to foolproof forms with Re-Captcha, which you can easily do with a plugin such as <a href="https://wordpress.com/plugins/advanced-google-recaptcha" rel="noreferrer"><strong><code>Advanced Google reCAPTCHA by WebFactory</code></strong></a>.</p><hr><p><strong>EDIT: </strong>A user queried: <em>&#xAB;I have some WordPress installations under Cloudflare and some exposing the server directly. Can it work on both?&#xBB;</em> </p><p>You can. It&apos;s as simple as doing something like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># match IP against Cloudflare list
acl from_cf src -f /usr/local/etc/haproxy/cloudflare_ips.lst

# if IP originates from cloudflare check CF-Connecting-IP header, otherwise use src
http-request set-var(txn.client_ip) hdr(CF-Connecting-IP) if from_cf
http-request set-var(txn.client_ip) src if !{ var(txn.client_ip) -m found }</code></pre><figcaption><p><span style="white-space: pre-wrap;">On previous examples, all IPs were from Cloudflare. Here it&apos;s obligatory to check if IP originates from Cloudflare. Otherwise, a spammer could forge </span><code spellcheck="false" style="white-space: pre-wrap;"><span>CF-Connecting-IP</span></code><span style="white-space: pre-wrap;"> and trick the simple if/else.</span></p></figcaption></figure><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/4e8v3DL"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2024/09/TP-Link-EAP610.jpg" alt="Mitigate attacks on WordPress running under Cloudflare - HAProxy and stick-tables"></a>
  </div>
  <div class="text">
    <p>
    This week, I assisted a friend in upgrading to professional <a href="https://amzn.to/4e8v3DL">TP-Link access points</a>. I&apos;m a strong advocate for devices that excel at a single task, and these EAP610 access points do just that. Highly recommended!
    </p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Premature optimization, where software thrives unless you kill it first - a tale of Java GC]]></title><description><![CDATA[Will a LinkedList be faster? Should I swap the `for each` with an `iterator`? Should this `ArrayList` be an `Array`? This article came to be in response to an optimization so malevolent it has permanently etched itself into my memory.]]></description><link>https://wasteofserver.com/premature-optimization-where-software-thrives-unless-you-kill-it-first-a-tale-of-java-gc/</link><guid isPermaLink="false">65f32ada5d7edd0001987360</guid><category><![CDATA[java]]></category><category><![CDATA[benchmark]]></category><category><![CDATA[programming]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Sat, 30 Mar 2024 17:45:49 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2024/03/java_future_gabage_collector_illustration.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2024/03/java_future_gabage_collector_illustration.jpg" alt="Premature optimization, where software thrives unless you kill it first - a tale of Java GC"><p>Before going heads on into Java and the ways to tackle interference, either from the garbage collector or from context switching, let&apos;s first glance over the fundamentals of writing code for your future self.</p><blockquote class="kg-blockquote-alt"> Premature optimization is the root of all evil.</blockquote><p>You&apos;ve heard it before; <em>premature optimization is the root of all evil</em>. Well, sometimes. When writing software, I&apos;m a firm believer of being:</p><p>1)<strong> </strong>as <strong>descriptive as possible</strong>; you should try to narrate intentions as if you were writing a story.</p><p>2) as <strong>optimal as possible</strong>; which means that you should know the fundamentals of the language and apply them accordingly.</p><h2 id="as-descriptive-as-possible"> As descriptive as possible</h2><p>Your code should speak intention, and a lot of it pertains to the way you name methods and variables.</p><figure class="kg-card kg-code-card"><pre><code class="language-java">int[10] array1;        // bad
int[10] numItems;      // better
int[10] backPackItems; // great</code></pre><figcaption><p><span style="white-space: pre-wrap;">Just by the variable name, you can already infer functionality</span></p></figcaption></figure><p>While <code>numItems</code> is abstract, <code>backPackItems</code> tells you a lot about expected behaviour.  </p><p>Or say you have this method:</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">List&lt;Countries&gt; visitedCountries() {
    if(noCountryVisitedYet)
        return new ArrayList&lt;&gt;(0);
    }
    // (...)
    return listOfVisitedCountries;
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">As far as code goes, this looks more or less ok.</span></p></figcaption></figure><p>Can we do better? We definitely can!</p><figure class="kg-card kg-code-card"><pre><code class="language-java">List&lt;Countries&gt; visitedCountries() {
    if(noCountryVisitedYet)
        return Collections.emptyList();
    }
    // (...)
    return listOfVisitedCountries;
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">Reading </span><code spellcheck="false" style="white-space: pre-wrap;"><span>Collections.emptyList()</span></code><span style="white-space: pre-wrap;"> is much more descriptive than </span><code spellcheck="false" style="white-space: pre-wrap;"><span>new ArrayList&lt;&gt;(0);</span></code></p></figcaption></figure><p>Imagine you&apos;re reading the above code for the first time and stumble on the <em>guard clause</em> that checks if the user has actually visited countries. Also imagine this is buried in a lengthy class, reading <code>Collections.emptyList()</code> is definitely more descriptive than <code>new ArrayList&lt;&gt;(0)</code>, you&apos;re also making sure it&apos;s immutable making sure client code can&apos;t modify it.</p><h2 id="as-optimal-as-possible">As optimal as possible</h2><p>Know your language and use it accordingly. If you need a <code>double</code> there&apos;s no need to   wrap it in a <code>Double</code> object. The same goes to using a <code>List</code> if all you actually need is an <code>Array</code>. </p><p>Know that you should concatenate Strings using <code>StringBuilder</code> or <code>StringBuffer</code> if you&apos;re sharing state between threads. </p><pre><code class="language-java">// don&apos;t do this
String votesByCounty = &quot;&quot;;
for (County county : counties) {
    votesByCounty += county.toString();
}

// do this instead
StringBuilder votesByCounty = new StringBuilder();
for (County county : counties) {
    votesByCounty.append(county.toString());
}
</code></pre><p>Know how to index your database. Anticipate bottlenecks and cache accordingly. All the above are optimizations. They are the kind of optimizations that you should be aware and implement as first citizens.</p><h2 id="how-do-you-kill-it-first">How do you kill it first?</h2><p>I&apos;ll never forget about a hack I read a couple of years ago. Truth be said, the author backtracked quickly, but it goes to show how a lot of evil can spur from good intention.</p><figure class="kg-card kg-code-card"><pre><code class="language-java">// do not do this, ever!
int i = 0;
while (i&lt;10000000) {
    // business logic
    
    if (i % 3000 == 0) { //prevent long gc
        try {
            Thread.sleep(0);
        } catch (Ignored e) { }
    }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">A garbage collector hack from hell!</span></p></figcaption></figure><p>You can read more on why and how the above code works <a href="https://levelup.gitconnected.com/why-does-my-colleague-use-thread-sleep-0-in-the-code-a3fd12dc98b7" rel="noreferrer">in the original article</a> and, while the exploit is definitely interesting, this is one of those things you should <strong>never </strong>ever<strong> </strong>do.</p><ul><li>Works by side effects, <code>Thread.sleep(0)</code> has no purpose in this block</li><li>Works by exploiting a deficiency of code downstream</li><li>For anyone inheriting this code, it&apos;s obscure and magical</li></ul><p>Only start forging something a bit more involved if, after writing with all the <strong>default optimizations the language provides</strong>, you&apos;ve hit a bottleneck. But steer away from concoctions as the above.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/03/garbage_collector.jpg" class="kg-image" alt="Premature optimization, where software thrives unless you kill it first - a tale of Java GC" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2024/03/garbage_collector.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2024/03/garbage_collector.jpg 1000w, https://wasteofserver.com/content/images/2024/03/garbage_collector.jpg 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">An interpretation of Java&apos;s future Garbage Collector &quot;imagined&quot; by Microsoft Copilot</span></figcaption></figure><h2 id="how-to-tackle-that-garbage-collector">How to tackle <em>that </em>Garbage Collector?</h2><p>If after all&apos;s done, the Garbage Collector is still the piece that&apos;s offering resistance, these are some of the things you may try:</p><ul><li>If your service is so latency sensitive that you can&apos;t allow for GC, run with <strong>&quot;Epsilon GC&quot; and avoid GC altogether</strong>.<br><code>-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC</code><br>This will obviously grow your memory until you get an OOM exception, so either it&apos;s a short-lived scenario or your program is optimized not to create objects<br><br></li><li>If your service is somewhat latency sensitive, but <strong>the allowed tolerance permits some leeway</strong>, run GC1 and feed it something like <code>-XX:MaxGCPauseTimeMillis=100</code> (default is 250ms)<br><br></li><li><strong>If the issue spurs from external libraries</strong>, say one of them calls <code>System.gc()</code> or <code>Runtime.getRuntime().gc()</code> which are stop-the-world garbage collectors, you can override offending behaviour by running with <code>-XX:+DisableExplicitGC</code><br><br></li><li>If you&apos;re running on a JVM above 11, do try the <a href="https://wiki.openjdk.org/display/zgc" rel="noreferrer">Z Garbage Collector (ZGC)</a>, performance improvements are monumental! <code>-XX:+UnlockExperimentalVMOptions -XX:+UseZGC</code>. You may also want to check this <a href="https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html" rel="noreferrer">JDK 21 GC benchmark</a>.</li></ul><table>
<thead>
<tr>
<th>Version Start</th>
<th>Version End</th>
<th>Default GC</th>
</tr>
</thead>
<tbody>
<tr>
<td>Java 1</td>
<td>Java 4</td>
<td>Serial Garbage Collector</td>
</tr>
<tr>
<td>Java 5</td>
<td>Java 8</td>
<td>Parallel Garbage Collector</td>
</tr>
<tr>
<td>Java 9</td>
<td>ongoing</td>
<td>G1 Garbage Collector</td>
</tr>
</tbody>
</table>
<p>Note 1: since Java 15, <code>ZGC</code> is <a href="https://wiki.openjdk.org/display/zgc/Main" rel="noreferrer">production ready</a>, but you still have to explicitly activate it with <code>-XX:+UseZGC</code>.</p><p>Note 2: The VM considers machines as server-class if the VM detects more than two processors and a heap size larger or equal to 1792 MB. If not server-class, it <a href="https://docs.oracle.com/en/java/javase/20/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781" rel="noreferrer">will default to the Serial GC</a>.</p><blockquote>In essence, opt for GC tuning when it&apos;s clear that the application&apos;s performance constraints are directly tied to garbage collection behavior and you have the necessary expertise to make informed adjustments. Otherwise, trust the JVM&apos;s default settings and focus on optimizing application-level code.<br><br><a href="https://www.reddit.com/user/shiphe/">u/shiphe</a> - you&apos;ll want to read the <a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxu9hff/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button" rel="noreferrer">full comment</a></blockquote><h2 id="other-relevant-libraries-you-may-want-to-explore">Other relevant libraries you may want to explore:</h2><h3 id="java-microbenchmark-harness-jmh"><a href="https://github.com/openjdk/jmh" rel="noreferrer">Java Microbenchmark Harness (JMH)</a></h3><p>If you&apos;re <em>optimizing</em> out of feeling without any real benchmarking, you&apos;re doing yourself a disservice. JMH is the <em>de facto</em> Java library to test your algorithms&apos; performance. Use it.</p><h3 id="java-thread-affinity"><a href="https://github.com/OpenHFT/Java-Thread-Affinity" rel="noreferrer">Java-Thread-Affinity</a></h3><p>Pinning a process to a specific core may improve cache hits. It will depend on the underlying hardware and how your routine is dealing with data. Nonetheless, this library makes it so easy to implement that, if a CPU intensive method is dragging you, you&apos;ll want to test it.</p><h3 id="lmax-disruptor"><a href="https://lmax-exchange.github.io/dis" rel="noreferrer">LMAX Disruptor</a></h3><p>This is one of those libraries that, even if you don&apos;t need, you&apos;ll want to study. The idea is to allow for ultra low latency concurrency. But the way it&apos;s implemented, from <em>mechanical sympathy</em> to the <em>ring buffer,</em> brings a lot of new concepts. I still remember when I first discovered it, seven years ago, pulling an all-nighter to digest it.</p><h3 id="netflix-jvmquake"><a href="https://github.com/Netflix-Skunkworks/jvmquake" rel="noreferrer">Netflix jvmquake</a></h3><p>The premiss of <code>jvmquake</code> is that when things go sideways with the JVM, you want it to die and not hang. A couple of years ago, I was running simulations on an HTCondor cluster that was on tight memory constraints and sometimes jobs would get stuck due to &quot;out of memory&quot; errors. This library forces the JVM to die, allowing you to deal with the actual error. On this specific case, HTCondor would auto re-schedule the job.</p><h2 id="final-thoughts">Final thoughts</h2><p>The code that made me write this post? I&apos;ve written way worse. I still do. The best we can hope for is to continuously mess up less. </p><p>I&apos;m expecting to be disgruntled looking at my own code a few years down the road. </p><p>And that&apos;s a good sign.</p><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/3TZrPLg"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2024/03/amazon_basics_shredder.jpg" alt="Premature optimization, where software thrives unless you kill it first - a tale of Java GC"></a>
  </div>
  <div class="text">
    <p>
    Given the nature of this post, I found it appropriate to promote a product I&apos;ve had for quite some time, <a href="https://amzn.to/3TZrPLg"><b>a home shredder!</b></a><br><br>
      
      This is the 3rd different model/brand I&apos;ve had and can definitely attest to Amazon Basics sturdiness. <br><br>

      While I do prefer signed and encrypted PDFs, there are a lot of financial institutions that still share data via paper. I shred those. Then I shred some other trivial stuff just to make it safe by obscurity.<br><br>
      
      In all honesty, I find it soothing.
      
    </p>
  </div>
</div>
<!--kg-card-end: html-->
<h2 id="edits-thank-you">Edits &amp; Thank You:</h2><ul><li>to <a href="https://www.florian-schaetz.de/" rel="noreferrer">FlorianSchaetz</a> for catching the mutability error in <code>visitedCountries()</code> and the detailed explanation.</li><li>to <a href="https://www.reddit.com/user/brunocborges/">u/brunocborges</a> and <a href="https://www.reddit.com/user/BikingSquirrel/" rel="noreferrer">u/BikingSquirrel</a> for explaining that <a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxtizb4/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button" rel="noreferrer">on lower end machines</a>, you get Serial GC</li><li>to <a href="https://www.reddit.com/user/shiphe/" rel="noreferrer">u/shiphe</a> for taking the time to better explain when you should mess with the GC and <a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxu9hff/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button" rel="noreferrer">when you shouldn&apos;t</a></li><li>to <a href="https://www.reddit.com/user/tomwhoiscontrary/">u/tomwhoiscontrary</a> to put me in the right track regarding what <a href="https://www.reddit.com/r/java/comments/1bubehn/comment/kxwln4u/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button" rel="noreferrer">should be considered standard</a> practice</li><li>to <a href="https://www.reddit.com/user/BikingSquirrel/">u/BikingSquirrel</a> (again) for providing the link to JDK 21 garbage collectors benchmark </li></ul>]]></content:encoded></item><item><title><![CDATA[How to calculate P&L]]></title><description><![CDATA[While displaying current profits or losses is a number taken for granted in every financial dashboard, it makes for a fun question as it's one of those calculations that can and should be optimized.]]></description><link>https://wasteofserver.com/how-to-calculate-pnl/</link><guid isPermaLink="false">65f10704caef9600014ac87e</guid><category><![CDATA[programming]]></category><category><![CDATA[trivial]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Thu, 07 Mar 2024 06:29:30 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2024/03/pnl_calculations.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2024/03/pnl_calculations.jpeg" alt="How to calculate P&amp;L"><p>Calculating profits and losses from a mathematical perspective is trivial, just subtract all things sold to all things bought. The remainder is your profit. That&apos;s pretty much how mom-and-pop retail works.</p><p>In future markets tough, people may get confused as you&apos;ll have open positions, which in retail would translate to inventory, that may not have been bought yet (if you&apos;re short selling).</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2024/03/pnl_calculations.jpeg" class="kg-image" alt="How to calculate P&amp;L" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2024/03/pnl_calculations.jpeg 600w, https://wasteofserver.com/content/images/size/w1000/2024/03/pnl_calculations.jpeg 1000w, https://wasteofserver.com/content/images/2024/03/pnl_calculations.jpeg 1024w" sizes="(min-width: 720px) 720px"></figure><p>Say you&apos;re trading in <code>Mini S&amp;P Futures</code> from CME.</p><p>The contract <code>lot size</code> is <code>50 units</code> and it&apos;s currently trading at around <code>5140</code>. Let&apos;s see how that works out in a simple BUY/SELL trade:</p><pre><code class="language-Java">// final value is num_contracts * lot_size * price
BUY  2 ESH4 @ 5140.00 &gt; 514_000 USD
SELL 2 ESH4 @ 5141.00 &gt; 514_100 USD
                           +100 USD (PNL)</code></pre><p>Let&apos;s add a third order to the mix to see how it works:</p><pre><code class="language-Java">BUY  2 ESH4 @ 5140.00 &gt; 514_000 USD
SELL 2 ESH4 @ 5141.00 &gt; 514_100 USD
SELL 2 ESH4 @ 5142.00 &gt; 514_200 USD</code></pre><p>If you think it through, you have an open position of <code>-2 ES</code> contracts that you&apos;ll eventually have to close. To calculate the instant P&amp;L you must assume current market price.</p><pre><code class="language-Java">BUY  2 ESH4 @ 5140.00 &gt; 514_000 USD
SELL 2 ESH4 @ 5141.00 &gt; 514_100 USD
SELL 2 ESH4 @ 5142.00 &gt; 514_200 USD
// current market price is 5145.00
// you have -2 ES contracts to close
BUY  2 ESH4 @ 5145.00 &gt; 514_500 USD
                           -200 USD (PNL)              </code></pre><p>Simple arithmetic. Buy orders are treated as negative values, sell orders are treated as positive values. That&apos;s really all you need to think about.</p><p>If you&apos;re not flat, you&apos;ll have a position that you must pretend to close (at current market value) to calculate your current P&amp;L.</p><pre><code class="language-Java">BUY  3 ESH4 @ 5100 = -3 * 50 * 5100
BUY  2 ESH4 @ 5150 = -2 * 50 * 5150
BUY  1 ESH4 @ 5200 = -1 * 50 * 5200
SELL 4 ESH4 @ 5300 = +4 * 50 * 5300
// contracts open (-3) - 2 - 1 + 4 = -2
                   = -2 * 50 * current_market_price</code></pre><p>So now that we&apos;ve covered the basics, you really only need to iterate through all the orders, count buys as negative, sells as positive, and, in the end, calculate whatever is open at the current market price.</p><h2 id="what-about-performance">What about performance?</h2><p>It&apos;s pretty evident that given a large amount of orders, doing this calculation for every single tick will become troublesome very fast. But as you&apos;ve probably guessed by now, the system only needs to do that calculation on <code>fills</code>. </p><p>As soon as an order gets filled, you calculate the P&amp;L for all filled positions and cache it. You also cache the number of contracts open. Then you just need to do a <strong>single calculation</strong> on price change:</p><p><code>pnl_realized + contracts_open * lot_size * current_market_price</code></p><p>If your system is not doing any sort of risk control, you can even push that calculation to the user GUI.</p><h2 id="commission-fees">Commission fees?</h2><p>Once you get the mechanics, they are very simple to add. Even if your current broker offers a flat fee, you&apos;ll want to delegate such calculation to its own class, as it will make code more extensible to future changes. I&apos;m thinking about quantity rebates, tiers, etc.</p><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/49IebBr"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2024/03/amazon-phone-holder.png" alt="How to calculate P&amp;L"></a>
  </div>
  <div class="text">
    <p>
    I&apos;ve recently been gifted this <a href="https://amzn.to/49IebBr">Amazon Basics phone holder</a>, and I&apos;m converted.</p>
    <p>I use it to browse online newspappers during breakfast and it also serves as a recipe holder in the kitchen. It has proven its worth daily. Highly recommended!
    </p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Interactive Brokers TWS API - and yet it works!]]></title><description><![CDATA[If you glance over the forums, it looks like this software was forged in the fires of hell. Follow me in a journey to the depths.]]></description><link>https://wasteofserver.com/interactive-brokers-tws-gateway-api-and-yet-it-works/</link><guid isPermaLink="false">65f10704caef9600014ac87d</guid><category><![CDATA[programming]]></category><category><![CDATA[java]]></category><category><![CDATA[crypto currencies]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Fri, 23 Feb 2024 04:59:43 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2024/02/trader_confused_with_TWS_API-1.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2024/02/trader_confused_with_TWS_API-1.jpeg" alt="Interactive Brokers TWS API - and yet it works!"><p>A friend asked if I could help as he was having a hard time integrating IKBR TWS API. Apparently, he&apos;s not alone:</p><blockquote><em>Even after all the above pain in my day job, I&apos;m still yet to come across an offering as crap as the TWS API. - </em><a href="https://www.reddit.com/r/interactivebrokers/comments/oyclx1/comment/h7sd2wy/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button"><em>DanWritesCode</em></a></blockquote><blockquote><em>I have been developing code for about 15 years, professionally, and have never seen a piece of garbage like the TWS Api. - </em><a href="https://www.reddit.com/r/interactivebrokers/comments/oyclx1/frustrated_with_tws_api/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button"><em>puzzled_orc</em></a></blockquote><blockquote><em>I put IBKR TWS not just at the bottom of the list, it&apos;s twice as bad as the most crappy system out there. - </em><a href="https://www.reddit.com/r/interactivebrokers/comments/syxxlv/comment/kc6nt81/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button"><em>Causal-Capital</em></a></blockquote><p>After reviewing TWS offer, it&apos;s definitely legacy code. </p><p>The API grew to support a multitude of requests in a non-standard and non-organized fashion. But <strong>legacy code, is also code that has been running for ages, most likely providing mission-critical functionality</strong>, and there&apos;s certainly value in that.</p><p>When you publish an API you&apos;re creating a contract with the world. As soon as it&apos;s out there, you can&apos;t really change it without creating ripples across the entire ecosystem. If requirements take an unexpected curve, as they generally do, it&apos;s easy to find yourself cornered and forced to provide a less than ideal interface.</p><p>What you can definitely do, is <strong>improve the documentation</strong> and <strong>provide clear code samples</strong> with what should be <strong>best practices</strong>. While I can&apos;t really point a finger at the API as I have no idea of the challenges involved at the time, <strong>the documentation could improve</strong>.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2024/02/trader_confused_with_TWS_API.jpeg" class="kg-image" alt="Interactive Brokers TWS API - and yet it works!" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2024/02/trader_confused_with_TWS_API.jpeg 600w, https://wasteofserver.com/content/images/size/w1000/2024/02/trader_confused_with_TWS_API.jpeg 1000w, https://wasteofserver.com/content/images/2024/02/trader_confused_with_TWS_API.jpeg 1024w" sizes="(min-width: 720px) 720px"></figure><h2 id="tws-connection">TWS Connection</h2><p>Let&apos;s see how you&apos;re supposed to integrate the API connection. Notice that per the API instructions <em>&#xAB;it is important that the main EReader object </em><a href="https://interactivebrokers.github.io/tws-api/connection.html#ereader"><em>is not created until after a connection has been established</em></a><em>&#xBB;, </em>which reads like misplaced behaviour.</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">Erapper wrapper = new EWrapperImpl();
EReaderSignal signal = new EJavaSignal();
EClientSocket client = new EClientSocket(wrapper, signal);

client.eConnect(&quot;host&quot;, PORT, CONNECTION_ID);</code></pre><figcaption><p><span style="white-space: pre-wrap;">Per the docs, this is a standard way to start TWS API</span></p></figcaption></figure><p>We can see that:</p><ul><li><code>wrapper</code> implements the interface where you&apos;ll get callbacks (i.e. receive things from the API, market data, fills, etc)</li><li><code>client</code> allows you to interact with TWS (i.e. send things to the API, orders, etc)</li></ul><p>And then we have both <code>signal</code> and <code>reader</code> classes.</p><ul><li><code>signal</code> is used to signal the reader thread that there&apos;s data to process</li><li><code>reader</code> processes incoming messages</li></ul><p>According to <a href="https://interactivebrokers.github.io/tws-api/connection.html#connect">IBKR documentation,</a> you should then start <code>reader</code> and a launch a new thread to process the queue:</p><pre><code class="language-Java">final EReader reader = new EReader(client, signal);   
reader.start();

// IBKR thread created solely to empty the messaging queue
new Thread(() -&gt; {
    while (client.isConnected()) {
        m_signal.waitForSignal();
        try {
            reader.processMsgs();
        } catch (Exception e) {
            // ...
        }
    }
}).start();</code></pre><p></p><p>I didn&apos;t dig up TWS code to see how it behaves internally, but from a birds-eye perspective, <strong>both <code>signal</code> and <code>reader</code> are misplaced </strong>and would be better encapsulated within the<strong> </strong><code>client</code>. </p><p>The user just needs a way to pass data to TWS (<code>client</code>) and a way to get data from TWS (<code>wrapper</code>). <strong>All other low level implementation details would better belong inside the implementation</strong>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/02/legacy_system.jpeg" class="kg-image" alt="Interactive Brokers TWS API - and yet it works!" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2024/02/legacy_system.jpeg 600w, https://wasteofserver.com/content/images/size/w1000/2024/02/legacy_system.jpeg 1000w, https://wasteofserver.com/content/images/2024/02/legacy_system.jpeg 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A legacy system connected to the future, by Microsoft Copilot</span></figcaption></figure><h2 id="broken-api-socket-connection">Broken API socket connection</h2><p>One of the places where my friend was having issues was in the re-connecting code. You should design to accommodate failure, much more so if you&apos;re developing an automated trading strategy.</p><p>The socket connection between your code and TWS will eventually break, so that&apos;s a good place to start. Glancing over TWS API we have that:</p><pre><code class="language-java">// creates socket connection to TWS/IBG.
client.eConnect(&quot;host&quot;, PORT, CONNECTION_ID);

// closes the socket connection and terminates its thread.
eDisconnect (bool resetState=true)</code></pre><p></p><p>If we break the socket at the network level, <a href="https://interactivebrokers.github.io/tws-api/connection.html#connect">like IKBR states</a>, we get a callback in the <code>wrapper</code> implementation:</p><pre><code class="language-Java">@Override
public void error(int id, int errorCode, String errorMsg, String advancedOrderRejectJson) {
    log.warn(&quot;error: id={}, errorCode={}, errorMsg={}, advancedOrderRejectJson={}&quot;, 
        id, errorCode, errorMsg, advancedOrderRejectJson);
}</code></pre><p></p><p>On a broken socket (network disconnect), the above will log something like:</p><pre><code class="language-properties">error: id=-1
errorCode=502
errorMsg=Couldn&apos;t connect to TWS. Confirm that &quot;Enable ActiveX and Socket
  Clients&quot; is enabled and connection port is the same as &quot;Socket Port&quot; on the
  TWS &quot;Edit-&gt;Global Configuration...-&gt;API-&gt;Settings&quot; menu. Live Trading ports:
  TWS: 7496; IB Gateway: 4001. Simulated Trading ports for new installations
  of version 954.1 or newer: TWS: 7497; IB Gateway: 4002, 
  advancedOrderRejectJson=null</code></pre><p></p><p>Notice that at this point we&apos;re not connected to TWS, but <code>client.isConnected()</code> still returns true. While not ideal, it&apos;s documented. You must issue a <code>eDisconnect()</code>.</p><pre><code class="language-Java">// pseudo code for broken socket behaviour

client.isConnected() -&gt; returns true
// here we break the socket
client.isConnected() -&gt; returns true
client.eDisconnect();
wrapper.connectionClosed(); // we get a callback here
client.isConnected() -&gt; returns false</code></pre><p></p><p>Given the above, it would look as though we could issue a <code>connect()</code> to re-establish connection.</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">client.eConnect(&quot;host&quot;, PORT, CONNECTION_ID);
wrapper.connectAck(); // we get a callback here
wrapper.connectionClosed(); // and then we get a callback here</code></pre><figcaption><p><span style="white-space: pre-wrap;">If we manually disconnect, we get a callback on </span><code spellcheck="false" style="white-space: pre-wrap;"><span>connectionClosed()</span></code><span style="white-space: pre-wrap;"> after re-connecting</span></p></figcaption></figure><p><strong>What&apos;s happening here?</strong></p><p>Remember the <code>thread</code> that we created to empty the queue? Let&apos;s revisit that:</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">new Thread(() -&gt; {
    while (client.isConnected()) { // when client disconnets it should exit
        signal.waitForSignal(); // but it&apos;s waiting on signal!
        try {
            reader.processMsgs();
        } catch (Exception e) {
            // ...
        }
    }
}).start();</code></pre><figcaption><p><span style="white-space: pre-wrap;">This code, which is provided as an example on how to empty the queue, has a massive red herring</span></p></figcaption></figure><p>Even though <code>client.isConnected()</code> returns false, we&apos;re stopped on <code>waitForSignal()</code> so the <code>while</code> loop won&apos;t exit!</p><p>As soon as the above thread gets a new signal, <code>client</code> is already connected and the above <strong>thread will not stop</strong>! This was creating havoc in my friend&apos;s code! As previously stated, all this behaviour could (should?) have been encapsulated upstream but, given that it&apos;s not, we can do it ourselves.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2024/02/isolate_behaviour.jpeg" class="kg-image" alt="Interactive Brokers TWS API - and yet it works!" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2024/02/isolate_behaviour.jpeg 600w, https://wasteofserver.com/content/images/size/w1000/2024/02/isolate_behaviour.jpeg 1000w, https://wasteofserver.com/content/images/2024/02/isolate_behaviour.jpeg 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Within the mess, we can isolate and encapsulate behaviour.</span></figcaption></figure><h2 id="a-proper-start-stop-reconnect-sequence">A proper start, stop &amp; reconnect sequence</h2><p>Per everything above, there is a meticulous sequence that you must follow for TWS to properly connect. You must create some objects, but some can only be created after specific callbacks have been received, etc. </p><p>When things start getting involved and into a spaghetti-like sequence, I recommend that you stick with the cleanest possible approach. In this case, it&apos;s a scenario where you create all required variables and another one where you reset them.</p><p><strong>Properly encapsulate Reader</strong></p><p>Making sure reader is properly initialized and terminated is a big part of the original issue, so we&apos;re isolating it to reduce clutter.</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">/**
 * Responsible for reading messages from the TWS socket.
 * &lt;p&gt;
 * Should be stopped when the connection to TWS is lost.
 */
public class Reader implements Runnable {
    private final AtomicBoolean running = new AtomicBoolean(true);

    private final EReaderSignal readerSignal;
    private final EReader reader;

    public Reader(EClientSocket clientSocket, EReaderSignal readerSignal) {
        this.readerSignal = readerSignal;
        reader = new EReader(clientSocket, readerSignal);
        reader.start();
    }

    @Override
    public void run() {
        while (running.get()) {
            readerSignal.waitForSignal();
            try {
                reader.processMsgs();
            } catch (Exception e) {
                // dispatch to error mechanism
            }
        }
    }

    public void stop() {
        running.set(false);
        readerSignal.issueSignal();
        reader.interrupt();
    }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">We can now control in a clean fashion when reader starts and stops</span></p></figcaption></figure><p>And then the calling class may look something like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">public class StartTwsApi {
    private static final Logger log = LoggerFactory.getLogger(Start.class);
    EWrapperImpl wrapper;
    EJavaSignal readerSignal;
    EClientSocket clientSocket;
    Executor executor = Executors.newSingleThreadExecutor();
    Reader reader;
    
    StartTwsApi() {
        initializeWrapperAndSocket();
        connectToTWS();
    }
    
    private void initializeWrapperAndSocket() {
        wrapper = new EWrapperImpl(this, this);
        readerSignal = new EJavaSignal();
        clientSocket = new EClientSocket(wrapper, readerSignal);
    }
    
    private void connectToTWS() {
        log.info(&quot;Connecting to TWS...&quot;);
        clientSocket.eConnect(&quot;localhost&quot;, 4005, 2);

        // safeguard to make sure we do connect / keep trying
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.schedule(() -&gt; {
            if (!clientSocket.isConnected()) {
                log.warn(&quot;Still not connected (10s). Retrying connect...&quot;);
                onTwsConnectionFailure();
            }
        }, 10, TimeUnit.SECONDS);
    }
    
    private void startReader() {
        executor.execute(reader);
    }
    
    private void stopReader() {
        if (reader != null) {
            reader.stop();
        }
    }
    
    /**
     * You&apos;ll call this method when you receive a callback from TWS 
     * stating that the connection is lost.
     * &lt;p&gt;
     * It stops the reader thread and re-connects to TWS.
     */
    public void onTwsConnectionFailure() {
        stopReader();
        log.info(&quot;Stopped reader. Will re-connect in 5 seconds...&quot;);
        Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
        connectToTWS();
    }
    
    /**
     * Call this method when you get a connectAck() on TWS 
     * wrapper callback implementation
     */
    public void connectAck() {
        log.info(&quot;connectAck received. Safe to start reader thread...&quot;);
        reader = new Reader(clientSocket, readerSignal);
        startReader();
    }
    
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">And even though it&apos;s still convoluted, now works as expected.</span></p></figcaption></figure><p>My recommendation is that you abstract all this behaviour under a specific TWS module, and then pass a cleaner interface back to your actual code.</p><p>There is much more that we&apos;ve delved on. Historical data, for instance, is an area where there were pretty interesting inconsistencies:</p><ul><li><code>public void historicalTicksBidAsk(int reqId, List&lt;HistoricalTickBidAsk&gt; ticks, boolean done)</code><br>tickData is received in a single callback as a List of Objects (max 1000)</li><li><code>public void historicalData(int reqId, Bar bar)</code><br>barData is received as a series of multiple callbacks with a single Bar object; when all bars arrive, you get a callback on <code>historicalDataEnd(int reqId, String startDateStr, String endDateStr)</code>.</li></ul><p>After a <a href="https://www.reddit.com/r/interactivebrokers/comments/1ay2aaa/comment/krxodv5/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">brief chat on Reddit with u/fit-interaction4450</a> I remembered another place where dragons lurk. After connecting you&apos;ll get a callback on <code>connectAck()</code> which is a signal to start the reader thread, but you must wait until another callback on <code>nextValidId(int orderId)</code> to actually start interacting with TWS.</p><p>And while IBKR <a href="https://interactivebrokers.github.io/tws-api/connection.html#connect">has it documented</a> (that&apos;s all we may ask for), it still speaks volumes to the way the API grew.</p><blockquote><em>Important: The <strong>IBApi.EWrapper.nextValidID</strong> callback is commonly used to indicate that the connection is completed and other messages can be sent from the API client to TWS. There is the possibility that function calls made prior to this time could be dropped by TWS.</em></blockquote><p>My friend, not having read the above, solved it by adding a <code>sleep()</code> after the connection. When I asked why that was there, he said that sometimes messages were not being received by TWS and pausing for a bit solved it. It sure did (most of the time, anyway), but caught my attention as a <code>sleep()</code> without a clear intention is, by definition, a code smell.</p><p><strong>Last thoughts:</strong></p><p>Designing an API is hard. Designing an API to stand the test of time is even harder. While I can identify antipatterns and a lack of behaviour guidelines, I may only assume that business needs and decisions dictated the curved approach we now see. <strong>The very same decisions that made IBKR into a 45 billion company</strong>.</p><p>I don&apos;t know if there&apos;s an audience for TWS API articles. If you want more, let me know in the comments below.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">In September, Interactive Brokers emailed wasteofserver to thank us for our article and suggested that we might consider contributing to their API, which is currently available on GitHub.<br><br>I must say that IBKR&#x2019;s approach was both commendable and flawless.</div></div><p></p><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/3vpwMTW"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2024/03/arctis_nova_7.png" alt="Interactive Brokers TWS API - and yet it works!"></a>
  </div>
  <div class="text">
    <p>
      My latest acquisition was the <a href="https://amzn.to/3vpwMTW">Arctis Nova 7 headphones</a>. Sound is so crispy you can immediately tell the difference between Spotify native app vs Spotify web!</p>
    <p>Microphone quality is also superb and has been praised in latest conference calls.</p>
    <p>If you&apos;re in the market for a new headset, I highly recommend checking out the Nova 7.</p>
<p></p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[SSH, Mosh and tmux]]></title><description><![CDATA[If you're using secure shell to remote, Mosh and tmux should be part of your arsenal.]]></description><link>https://wasteofserver.com/ssh-mosh-and-tmux/</link><guid isPermaLink="false">65f10704caef9600014ac87b</guid><category><![CDATA[utilities]]></category><category><![CDATA[trivial]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Mon, 30 Oct 2023 18:00:22 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2023/10/ssh_mosh_tmux.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2023/10/ssh_mosh_tmux.jpeg" alt="SSH, Mosh and tmux"><p>In one way or another, this post has been written thousands of times across the web. I&apos;m <strong>rewriting it for two reasons:</strong> </p><ul><li>Command reference </li><li><code>mosh</code> under <code>Cygwin</code>error.</li></ul><h3 id="mosh-mobile-shell">Mosh (mobile shell)</h3><p><a href="https://mosh.org/#usage">Mosh</a> uses UDP to create an unbreakable ssh session. If you&apos;re on the road with flaky connections, it&apos;s a lifesaver.</p><p>On Windows, you can use <code>mosh</code> either in WSL or Cygwin. If using via Cygwin, you may stumble on the error <em>&#xAB;Did not find remote IP address (is SSH ProxyCommand disabled?)&#xBB;.</em></p><pre><code class="language-shellsession">$ ./mosh frankie@192.168.5.2
CreateProcessW failed error:2
posix_spawnp: No such file or directory
./mosh: Did not find remote IP address (is SSH ProxyCommand disabled?).</code></pre><p>To fix it, it&apos;s a simple as passing a few extra parameters:</p><pre><code class="language-shellsession">$ ./mosh --predict=always --experimental-remote-ip=remote frankie@192.168.5.2</code></pre><p><code>--predict=</code>controls use of speculative local echo<br><code>--experimental-remote-ip=</code> used to discover IP address mosh connects to</p><p>Notice that if you&apos;re having errors connecting, you should check if you&apos;re actually allowed to UDP into the server. For that, use <code>netcat</code>.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">nc -u &lt;host&gt; &lt;port&gt;</code></pre><figcaption><p><span style="white-space: pre-wrap;">check connection to mosh server (generally port 60001)</span></p></figcaption></figure><h3 id="tmux-terminal-multiplexer">tmux (terminal multiplexer)</h3><p><a href="https://github.com/tmux/tmux/wiki">tmux</a> puts your terminal connection into a proper session. Say you&apos;re working on a remote server from your home. You can disconnect that session. Go to work and resume the session exactly as it was.</p><p>On top of that, it allows you to easily split the screen.</p><p><code>tmux new</code> - to start<br><code>tmux new -s session-name</code> - to name the session<br><code>tmux -t session-name</code> - to attach to the named session</p><p>During the session, your go-to command is <code>Ctrl-b</code>.</p><p><code>Ctrl+b %</code> - splits the screen vertically<br><code>Ctrl+b &quot;</code> - splits the screen horizontally<br><code>Ctrl+b x</code> - closes current session</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/10/mosh_tmux_better_ssh.jpeg" class="kg-image" alt="SSH, Mosh and tmux" loading="lazy" width="1024" height="1024" srcset="https://wasteofserver.com/content/images/size/w600/2023/10/mosh_tmux_better_ssh.jpeg 600w, https://wasteofserver.com/content/images/size/w1000/2023/10/mosh_tmux_better_ssh.jpeg 1000w, https://wasteofserver.com/content/images/2023/10/mosh_tmux_better_ssh.jpeg 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Enjoy your super-powered ssh connection!</span></figcaption></figure><hr>
<!--kg-card-begin: html-->
<div class="float_left">
  <div style="margin: 0 20px 20px 20px; float: left; max-width: 50%">
      <a href="https://amzn.to/3IAVUdq"><img style="max-width: 100%; margin: 0" src="https://wasteofserver.com/content/images/2024/03/output-onlinepngtools.png" alt="SSH, Mosh and tmux"></a>
  </div>
  <div class="text">
    <p>
    I&apos;ve never had one of these iPhone cables fail.<br><br> Even after being chewed up by Roomba. It&apos;s funny how <a href="https://amzn.to/3IAVUdq">Amazon does a better iPhone cable</a> than Apple, but there you have it. Can&apos;t recommend them enough. </p>
  </div>
</div>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Pepsi broke the contract]]></title><description><![CDATA[An API is a contract with the world, and that makes it one of the hardest things to get right in software design.]]></description><link>https://wasteofserver.com/pepsi-broke-the-contract/</link><guid isPermaLink="false">65f10704caef9600014ac877</guid><category><![CDATA[trivial]]></category><category><![CDATA[life hack]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Mon, 20 Mar 2023 04:57:42 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2023/03/bruno-kelzer-rbLJ0EPyjAM-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2023/03/bruno-kelzer-rbLJ0EPyjAM-unsplash.jpg" alt="Pepsi broke the contract"><p>Some APIs change because the world evolves, some because they are fundamentally broken from the start; looking at you <a href="https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html"><code>Thread.stop</code></a>!</p><p>What makes API design hard is that it must endure time. As soon as the contract is out there, you can&apos;t just take it back without a world of pain. Stadia is a perfectly good example of <a href="https://arstechnica.com/gaming/2022/10/meet-the-stadia-developers-blindsided-by-googles-latest-product-shutdown/">lost trust</a>.</p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2023/07/unhappy-pepsi-bottle-surrounded-by-money.jpg" class="kg-image" alt="Pepsi broke the contract" loading="lazy" width="983" height="968" srcset="https://wasteofserver.com/content/images/size/w600/2023/07/unhappy-pepsi-bottle-surrounded-by-money.jpg 600w, https://wasteofserver.com/content/images/2023/07/unhappy-pepsi-bottle-surrounded-by-money.jpg 983w" sizes="(min-width: 720px) 720px"></figure><h2 id="whats-with-a-recipe">What&apos;s with a recipe?</h2><p>Both Coca-Cola and Pepsi have iterated their formulas over the years. It&apos;s a known fact that one of the major ingredients in Coke is now missing;<strong> coca</strong>-leave (cocaine) and <strong>cola</strong>-nut (caffeine) gave Coca-Cola its name. </p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Did you know that a single Illinois company, Stepen (NYSE:SCL), has DEA approval to import coca-leaves? They come from Bolivia or Peru and, after cocaine removal, Coca-Cola buys the leftovers to flavour its soft drinks.</div></div><p></p><p>The 70s saw the commodities boom and, with it, the last formula change.</p><p>In the aftermath of the six-days war, Egypt closed the Suez Canal, which stirred the oil crises. There was also a massive spike in Coffee price given that Brazil lost 2/3rds of its coffee plants and both Angola and Guatemala saw a civil war and a major earthquake. </p><p>Amidst the instability, the largest sugar producer at the time, the Soviet Union, started hoarding the carbohydrate to fend off further uncertainty.</p><p><strong>That was the last change Pepsi and Coca-Cola made: </strong>high-fructose corn syrup. Until now.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F33D;</div><div class="kg-callout-text">Not all soda is corn-syrup based. Some countries kept using sugar. Most soda factories in Europe use beet sugar, whereas those in South America preferentially use cane sugar.</div></div><h2 id="whats-up-with-pepsi-api-then">What&apos;s up with Pepsi API, then?</h2><p>Given that for the last 50 years there was an implicit contract, it came as a surprise to learn that PepsiCo was sneakily messing with the original drink&apos;s recipe.</p><p>The idea is pretty smart, but for it to work, Pepsi can&apos;t make any fuss around it. The splashy campaigns of the past gave way to a covertness operation. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/03/old_pepsi_can.jpg" class="kg-image" alt="Pepsi broke the contract" loading="lazy" width="400" height="723"><figcaption><span style="white-space: pre-wrap;">As of March 2023 Pepsi Original has only 15g of sugar per 330ml can. Less than half what it originally had!</span></figcaption></figure><p>During the pandemic, Pepsi Original, the blue can, saw a remake. On the outside, everything stayed surreptitious the same. On the inside, though, a tiny bit of sugar was replaced by AcesulfameK.</p><p>The idea is to keep replacing sugar by AcesulfameK in tiny amounts and, eventually,<strong> Pepsi Original will be sugar-free.</strong> Or not. Depends on market reaction.</p><p>On the few occasions PepsiCo talked about the change, it did so under the ruse of promoting a healthier lifestyle and without conceding a timeline. My best guess is that they&apos;ll be monitoring sales to see how far can they go.</p><p>But let&apos;s be honest, the end goal is profit and sugar-tax, which is not yet a thing in the US, eats a lot of profit in Europe.</p><p>In 2017 Pepsi sold 127 million litres of Pepsi original in the UK alone. <strong>In sugar-tax only, this amounts to over 10 million pounds.</strong> Coca-cola will pay more than 22 million of sugar tax just in the UK!</p><h2 id="sugar-is-bad-so-this-should-be-good-right">Sugar is bad, so this should be good, right?</h2><p>Let&apos;s get back to the API. When you implement a public contract, your consumers, fittingly, have a certain degree of expectation. You will most definitely have an EULA that gives you free reigns over the system, but having people use your product is the end goal. As such, backward incompatible change will be frowned upon and, if it must be so, has to be explicit and perfectly documented.</p><p>Pepsi&apos;s strategy to bypass the sugar-tax is precisely the opposite.</p><p>You may argue that the majority of users will not notice and, if you&apos;re being pedantic, that it wasn&apos;t an API change, just a data change. While most people will definitely not notice, there&apos;s a minority whose systems will break.</p><p><strong>People who are allergic to AcesulfameK and Aspartame!</strong></p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F575;&#xFE0F;&#x200D;&#x2640;&#xFE0F;</div><div class="kg-callout-text">A lifetime ago, I pursued a healthier lifestyle by switching to zero sugar sodas. As symptoms only showed up three weeks after the change, I missed the connection.A constant need to pee. So bad it troubled my sleep and messed my work. A visit to the urologist and a multitude of exams later, I was running on empty. &quot;Stress&quot; and &quot;the bladder is the <a href="https://en.wikipedia.org/wiki/Sentinel_species#Canaries_in_coal_mines">canary in the coal mine</a> of the human body&quot; didn&apos;t bring resolve.One day, out of despair, I gave way to bad habits of before. Two weeks later I was perfectly fine. Took me a relapse into zero-sugar drinks to make the connection!I was lucky.I got to know people who endured decades of misery before realizing Canderel&#xAE; or similar, messed them up.</div></div><h2 id="embrace-change-but-engage-users">Embrace change, but engage users!</h2><p>A good API is designed taking into consideration the consumer&apos;s needs. It should be explicit and allow for seamless integration. It should be complete. Concise and hard to misuse.</p><p>Be a good API designer. </p><p>Don&apos;t be like Pepsi.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/03/coca-cola-in-teotihuacan.jpg" class="kg-image" alt="Pepsi broke the contract" loading="lazy" width="2000" height="1333" srcset="https://wasteofserver.com/content/images/size/w600/2023/03/coca-cola-in-teotihuacan.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2023/03/coca-cola-in-teotihuacan.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2023/03/coca-cola-in-teotihuacan.jpg 1600w, https://wasteofserver.com/content/images/2023/03/coca-cola-in-teotihuacan.jpg 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">By the very talented Jordan Crawford. Check out </span><a href="https://twitter.com/inventitorfixit"><span style="white-space: pre-wrap;">his latest creations</span></a><span style="white-space: pre-wrap;"> on Twitter.</span></figcaption></figure><p>Out of spite, <a href="https://amzn.to/3n3vp99">get yourself some Coca-Cola</a>.</p><p><strong>Edit:</strong> There&apos;s a <a href="https://www.reddit.com/r/cocacola/comments/11w9bdd/pepsi_broke_the_contract/">Reddit thread</a> where users say that in the US, Pepsi is still using the original formula. On top of that, in Peru (a sugar-tax country), Coca-Cola is following Pepsi&apos;s lead by feeling the markets&apos; reaction to changing the formula without warning users.</p><p><strong>Edit 2025: </strong>A user shared this chart. While it may not align perfectly with the specific focus of this post given PepsiCo&#x2019;s broader scope, the timeline somewhat corresponds.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2025/08/coca_cola_vs_pepsi_10y_market_cap.png" class="kg-image" alt="Pepsi broke the contract" loading="lazy" width="1200" height="846" srcset="https://wasteofserver.com/content/images/size/w600/2025/08/coca_cola_vs_pepsi_10y_market_cap.png 600w, https://wasteofserver.com/content/images/size/w1000/2025/08/coca_cola_vs_pepsi_10y_market_cap.png 1000w, https://wasteofserver.com/content/images/2025/08/coca_cola_vs_pepsi_10y_market_cap.png 1200w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Pepsi recipe change was in early 2023.</span></figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Run bash commands from Java]]></title><description><![CDATA[You may want to use `ffmpeg` to convert a bunch of videos, or call `ImageMagick` to manipulate some images. Whatever you may need, calling external programs from Java is pretty straightforward.]]></description><link>https://wasteofserver.com/call-external-command-from-java/</link><guid isPermaLink="false">65f10704caef9600014ac875</guid><category><![CDATA[java]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Wed, 01 Mar 2023 18:06:38 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2023/03/bash_computer_promp.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2023/03/bash_computer_promp.jpg" alt="Run bash commands from Java"><p>As one of Java paradigms is &quot;write once, run anywhere&quot;, and calling external software makes that much more complicated, you&apos;ll want to steer away from it as much as possible. However, sometimes there&apos;s really no other viable option.</p><p>The example below shows how to ping a host and capture both the standard and the error output streams.</p><figure class="kg-card kg-code-card"><pre><code class="language-java">String cmd = &quot;ping -c 4 -W 1 8.8.8.8&quot;

try {
    ProcessBuilder pb = new ProcessBuilder(&quot;bash&quot;, &quot;-c&quot;, cmd);
    pb.redirectErrorStream(true);
    Process process = pb.start();
    process.waitFor(10, TimeUnit.SECONDS);
    try (BufferedReader br = new BufferedReader(
        new InputStreamReader(process.getInputStream())
    )) {
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = br.readLine()) != null) {
            stringBuilder.append(line);
        }
        if (process.exitValue() != 0) {
            // mail sending failed, error is on stringBuilder
        }
    }
} catch (IOException | InterruptedException e) {
	// recover / etc
}</code></pre><figcaption><code>-c 4</code> send 4 packets and <code>-W 1</code> waits 1 second max for each packet.</figcaption></figure><p>On my email sending class, I generally have a fallback that relies on the machine sendmail/mail program. It kicks in if the SMTP server is not responding or is badly configured.</p><p>You may be interested in checking <a href="https://wasteofserver.com/sendmail-with-subject-on-a-single-line/">how to use sendmail (or mail) for sending emails as a one-liner</a>.</p>]]></content:encoded></item><item><title><![CDATA[Sony WF-1000XM4 vs Apple AirPods Pro]]></title><description><![CDATA[Sony, a behemoth of the musical industry versus Apple that, since the iPod debut, stuck a chord with the EarPods and subsequent upgrades.]]></description><link>https://wasteofserver.com/sony-wf-1000xm4-vs-apple/</link><guid isPermaLink="false">65f10704caef9600014ac874</guid><category><![CDATA[reviews]]></category><category><![CDATA[hardware]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Thu, 02 Feb 2023 04:50:25 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg" alt="Sony WF-1000XM4 vs Apple AirPods Pro"><p>While this website is not about reviews, I try to command it in the same spirit I used to navigate Usenet, if it may be useful, it should be shared.</p><p>I&apos;ve been lucky enough to be gifted both these earphones, as disappointed previous owners, disposed them my way. </p><figure class="kg-card kg-image-card"><img src="https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg" class="kg-image" alt="Sony WF-1000XM4 vs Apple AirPods Pro" loading="lazy" width="1978" height="1156" srcset="https://wasteofserver.com/content/images/size/w600/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg 1600w, https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony-WF-1000XM4-.jpg 1978w" sizes="(min-width: 720px) 720px"></figure><p>The AirPods belonged to my wife that unfortunately could not get a proper fit. The WF-1000XM4 were offered by a brother that&apos;s an audiophile and upgraded to some very expensive, custom-tip made, ones.</p><p>After one month of commutes, extensive hours in the tube, 8 aeroplane rides, multiple jogs, 6 hours Zwifting and many more typing away, I have a pretty solid opinion on what works. For concrete details on fidelity and ample scrutiny, you should read what <a href="https://www.soundguys.com/apple-airpods-pro-vs-sony-wf-1000xm4-53722/">the soundguys wrote</a>, the critic below is just my biased analysis of having using them both for a considerable amount of time.</p><table>
<thead>
<tr>
<th style="text-align:right"></th>
<th>AIRPODS PRO</th>
<th>WF-1000XM4</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right"><strong>back type</strong></td>
<td>open</td>
<td>closed</td>
</tr>
<tr>
<td style="text-align:right"><strong>weight (each bud)</strong></td>
<td>5.4 g</td>
<td>7.3 g</td>
</tr>
<tr>
<td style="text-align:right"><strong>driver</strong></td>
<td>11 mm</td>
<td>6 mm</td>
</tr>
<tr>
<td style="text-align:right"><strong>tips made of</strong></td>
<td>silicon</td>
<td>memory foam</td>
</tr>
</tbody>
</table>
<h2 id="sound-quality">Sound Quality</h2><p>Just by looking at the specs, given the difference in driver&apos;s size, I&apos;d thought Apple would have the upper hand. The SoundGuys also prove that, objectively, the AirPods are more accurate. My experience though is mixed. I found the WF-1000XM4 extremely pleasant to listen to music. Bass had punch and trebles were detailed. Personally, I&apos;d say it&apos;s a close match. Perhaps leaning towards Sony.</p><h2 id="noise-cancelling">Noise-cancelling</h2><p>Sony is king in aeroplanes. Noise-cancelling is just on another level. With a closed back and memory foam tips, you get incredible passive noise-cancelling. When you add active suppression to that, it just blows the AirPods away.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_size_compare.jpg" class="kg-image" alt="Sony WF-1000XM4 vs Apple AirPods Pro" loading="lazy" width="2000" height="1270" srcset="https://wasteofserver.com/content/images/size/w600/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_size_compare.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_size_compare.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_size_compare.jpg 1600w, https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_size_compare.jpg 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Bulkier, but much quieter.</span></figcaption></figure><h2 id="comfort">Comfort</h2><p>The 35% increase in weight isn&apos;t noticeable, but I can&apos;t wear them for more than a couple of hours at a time because I get tired. For extended use, nothing beats over-ear headphones.</p><p>I noticed that the Sony headphones took longer to fit because of the memory foam. When I needed to quickly take a phone call, I often chose the AirPods since they were faster to put on.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_tip_compare.jpg" class="kg-image" alt="Sony WF-1000XM4 vs Apple AirPods Pro" loading="lazy" width="2000" height="1477" srcset="https://wasteofserver.com/content/images/size/w600/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_tip_compare.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_tip_compare.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_tip_compare.jpg 1600w, https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_tip_compare.jpg 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Sony&apos;s larger tip is made of memory-foam. It expands and helps block the outside world.</span></figcaption></figure><h2 id="sports">Sports</h2><p>None of these is an actual sports earphone. I&apos;m a fan of <a href="https://amzn.to/40l1Gri">Bose&apos;s SoundSport</a> which has a cable around the neck and large controls you can easily manage while running. Unfortunately, most brands discontinued cable earphones. </p><p>I&apos;m yet to try the new Bose Sport Earbuds, so I can&apos;t comment on those, but one thing&apos;s for sure, the AirPods Pro and the WF-1000XM4 fall short of proper sport headphones.</p><p>There&apos;s a major difference though, while I could (and did!) take the AirPods for a jog, <strong>I could not even pretend to try and run with the WF-1000XM4</strong>. Having both a closed back and memory foam, Sony&apos;s earbuds create an airtight compartment in your ear canal. That seal, the reason why they are fantastic at noise-cancelling, is also their undoing for sports. </p><p>When you run (at least when I run), there is a slight movement in your ears as you hit the asphalt. That motion also wiggles the earphones. Regrettably, as the WF-1000XM4 are airtight, that shift creates a difference in pressure that directly hits your tympanums. Painful.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_back_compare-3.jpg" class="kg-image" alt="Sony WF-1000XM4 vs Apple AirPods Pro" loading="lazy" width="2000" height="1500" srcset="https://wasteofserver.com/content/images/size/w600/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_back_compare-3.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_back_compare-3.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_back_compare-3.jpg 1600w, https://wasteofserver.com/content/images/2023/02/apple_airpods_pro_vs_sony_Sony_WF-1000XM4_back_compare-3.jpg 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">AirPods controls are in the protruding &quot;stick&quot;, while the WF-1000XM4 ones are on the backplate. Sometimes, hitting those controls, also put pressure in my tympanums.</span></figcaption></figure><h2 id="aerodynamics-added-august-2024">Aerodynamics (added August 2024)</h2><p>Lately, I&#x2019;ve been cycling a lot and noticed that using the WF-1000XM4s against the wind significantly increases the noise. Even active noise-canceling can&#x2019;t damp a small breeze. The buzz of the wind rushing past the oddly shaped Sony earbuds is deafening. In this regard, the AirPods Pro are the clear winner.</p><h2 id="verdict">Verdict</h2><p>After one month, I found myself leaving home exclusively with the AirPods Pro. They&apos;re more versatile, faster to engage and, letting some outside noise in, makes them safer on the streets.</p><p>I&apos;d say Sony designed the WF-1000XM4 with a specific purpose, whereas Apple made an all-round product.</p><p><a href="https://amzn.to/3wMgt0v">Sony&apos;s WF-1000XM4</a> - 170 USD<br><a href="https://amzn.to/40o95pS">Apple&apos;s AirPods Pro</a> - 190 USD</p><p>Hope this helps steer your choice.</p><h2 id="edit-noise-cancelling-chart">Edit: Noise Cancelling Chart</h2><p>After a question from <a href="https://www.reddit.com/user/CorvusCascadia/">u/CorvusCascadia</a> I&apos;ve superimposed both charts from the <a href="https://www.soundguys.com/apple-airpods-pro-vs-sony-wf-1000xm4-53722/">soundguys.com</a> so that we may better visualize the surreal difference in noise attenuation.</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2023/02/noice_cancel_sony_vs_apple.jpg" class="kg-image" alt="Sony WF-1000XM4 vs Apple AirPods Pro" loading="lazy" width="1852" height="881" srcset="https://wasteofserver.com/content/images/size/w600/2023/02/noice_cancel_sony_vs_apple.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2023/02/noice_cancel_sony_vs_apple.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2023/02/noice_cancel_sony_vs_apple.jpg 1600w, https://wasteofserver.com/content/images/2023/02/noice_cancel_sony_vs_apple.jpg 1852w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">The WF-1000XM4, in passive mode, cancel more noise than the AirPods Pro in active mode!</span></figcaption></figure><hr><figure class="kg-card kg-image-card kg-card-hascaption"><a href="https://amzn.to/4dpOYh8"><img src="https://wasteofserver.com/content/images/2024/08/airpods_pro_2-1.png" class="kg-image" alt="Sony WF-1000XM4 vs Apple AirPods Pro" loading="lazy" width="1200" height="630" srcset="https://wasteofserver.com/content/images/size/w600/2024/08/airpods_pro_2-1.png 600w, https://wasteofserver.com/content/images/size/w1000/2024/08/airpods_pro_2-1.png 1000w, https://wasteofserver.com/content/images/2024/08/airpods_pro_2-1.png 1200w" sizes="(min-width: 720px) 720px"></a><figcaption><span style="white-space: pre-wrap;">Apple AirPods Pro 2 are our current recommendation</span></figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Java: How to get all implementations of an Interface]]></title><description><![CDATA[You've implemented them all. That was the hard part. Listing them should be easy!]]></description><link>https://wasteofserver.com/java-how-to-get-all-implementations-of-an-interface/</link><guid isPermaLink="false">65f10704caef9600014ac873</guid><category><![CDATA[java]]></category><dc:creator><![CDATA[frankie]]></dc:creator><pubDate>Wed, 28 Dec 2022 05:17:34 GMT</pubDate><media:content url="https://wasteofserver.com/content/images/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://wasteofserver.com/content/images/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash-1.jpg" alt="Java: How to get all implementations of an Interface"><p>How to list all known implementations of an interface is something that I&apos;m asked a lot. I&apos;m biased towards two ways of doing it.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://wasteofserver.com/content/images/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash.jpg" class="kg-image" alt="Java: How to get all implementations of an Interface" loading="lazy" width="2000" height="1125" srcset="https://wasteofserver.com/content/images/size/w600/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash.jpg 600w, https://wasteofserver.com/content/images/size/w1000/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash.jpg 1000w, https://wasteofserver.com/content/images/size/w1600/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash.jpg 1600w, https://wasteofserver.com/content/images/size/w2400/2022/12/samuel-horn-af-rantzien-8PKEeac1hzw-unsplash.jpg 2400w" sizes="(min-width: 720px) 720px"><figcaption>By the very talented <a href="https://twitter.com/pixelcrook">Samuel Horn af Rantzien</a></figcaption></figure><p>Java 6 brought us the Service Provider Interface (SPI) which has at its core the ServiceLoader class, responsible for loading implementations.</p><p>Java itself uses the Service Provider Interface (SPI) in many ways:</p><ul><li><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/spi/CurrencyNameProvider.html">CurrencyNameProvider</a></li><li><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/text/spi/NumberFormatProvider.html">NumberFormatProvider</a></li><li><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/spi/TimeZoneNameProvider.html">TimeZoneNameProvider</a></li></ul><p>Using it is straightforward. You just have to declare all providers in a provider configuration file. In order to do so, put them in a <code>META-INF/services/com.example.providers.CustomProvider</code> and the content of this file will list the existing providers:</p><figure class="kg-card kg-code-card"><pre><code class="language-java">com.example.providers.ProviderOfFood
com.example.providers.ProviderOfHappiness</code></pre><figcaption>File contents are the fully qualified name(s) of the implementations.</figcaption></figure><p>I don&apos;t really like the above solution, it&apos;s brittle and prone to errors. I&apos;ve seen programs where providers were not available as there was a typo, and I&apos;ve also seen lost hours because developers refactored classes forgetting to update the respective <code>META-INF</code> files.</p><p><strong>Both libraries below achieve the same.</strong></p><p>Say you have the following code:</p><figure class="kg-card kg-code-card"><pre><code class="language-java">public interface Provider {
    void sayHello();
}

@AutoService(Provider.class)
public class ProviderOfFood implements Provider {
    ...
}

@AutoService(Provider.class)
public class ProviderOfHappiness implements Provider {
    ...
}</code></pre><figcaption>Two implementations of the hypothetical <code>Provider</code> interface</figcaption></figure><h2 id="google-autoservice">Google AutoService</h2><p>At compile time, <a href="https://github.com/google/auto/tree/main/service">Google AutoService</a> searches your code for annotations of type <code>@AutoService</code> and automatically writes the <code>META-INF</code> file for you. This implementation, having the search at compile time, is the fastest one. You would call it like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-java">ServiceLoader&lt;Provider&gt; providers = ServiceLoader.load(Provider.class);
for (Provider p : providers) {
    p.sayHello();
}</code></pre><figcaption>You simply use Java&apos;s ServiceLoader to get your implementations</figcaption></figure><h2 id="ronmano-reflections"> Ronmano Reflections</h2><p>Finds your classes at runtime using reflections. To make faster, you must narrow down the search scope by providing some package info to the library. <a href="https://github.com/ronmamo/reflections">This implementation</a> has the benefit of working better with hot-reloaded code. Though you&apos;ll pay the price in speed. You would call it like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-java">Reflections reflections = new Reflections(&quot;com.example.providers&quot;);
Set&lt;Class&lt;?&gt;&gt; subTypes = reflections.get(SubTypes.of(Provider.class).asClass());

for (Class&lt;?&gt; aClass : subTypes) {
    Constructor&lt;?&gt; ctor = aClass.getConstructor();
    Provider p = (Provider) ctor.newInstance();
    p.sayHello();
}</code></pre><figcaption>Notice that you specified <code>com.example.providers</code> as the location to search for providers</figcaption></figure><h2 id="which-library-should-we-use">Which library should we use?</h2><p>Depends. </p><p>ServiceLoader is <strong>10x faster</strong> than Ronmano Reflections, but even on very modest hardware, running Ronmano Reflections 10_000 times will only take you back an extra ~8 seconds.</p><p>When you use ServiceLoader you initialize the classes. If you just want to list them, you can read the <code>META-INF</code> file. </p><p>Using Ronmano Reflections tough, you get the list of the classes, and you have to initialize them by hand. That, depending on the situation, may be a plus.</p><hr><p>I&apos;m a fan of mechanical keyboards, but this season gifted some <a href="https://amzn.to/3jmHB2H">LogicTech K120 to family and friends</a> who I knew had outrageously worn out keyboards. Huge success! If I can&apos;t offer a perishable item, I try to make it something that wears out. I&apos;ve learnt my lesson on decorative articles a long time ago! ;)</p>]]></content:encoded></item></channel></rss>